######################################## SOH ###########################################
## Function : Alternate version for Tk:Optionbox with scroller, better for larger amounts
##            of selectable items
##
## Copyright (c) 2008 Michael Krause. All rights reserved.
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##
## History  : V0.01	27-Feb-2008 	Class derived from original Optionbox. MK
##            V0.02 03-Mar-2008 	First usable version
##
######################################## EOH ###########################################
package Tk::PopUpSelectBox;
use Tk;

########################################################################
# ResizeCBox is a a slightly modified version of Damion K. Wilson's
# CornerBox (derived from Tk-DKW-0.03) Thanx.
########################################################################

package Tk::PopUpSelectBox::ResizeCBox;

use Tk::Canvas;
use Tk::Frame;

use vars qw ($VERSION);
use base qw (Tk::Frame);

$VERSION = '0.01';

#---------------------------------------------
use constant TRIMCOUNT	=> '3';
use constant MINSIZE_X	=> '100';
use constant MINSIZE_Y	=> '50';

#---------------------------------------------
Tk::Widget->Construct ('ResizeCBox');

#---------------------------------------------
sub Populate
{
	my ($this, $args) = @_;

	# retrieve extra option
	my $size = $this->{_size} = delete $args->{-size} || 20; $size = 15 if $size < 15;

    $this->SUPER::Populate (@_);

	#Widget Creation
	my $canvas = $this->Canvas(
    	-borderwidth => 0,
    	-highlightthickness => 0,
		-height => $size,
    	-width => $size,
	)->pack(
    	-fill => 'both',
    	-expand => '1',
	);
	$this->Advertise('canvas' => $canvas);

    $canvas->Tk::bind ('<ButtonPress-1>' => [\&Press, $this]);
    $canvas->Tk::bind ('<B1-Motion>' => [\&Resize, $this]);
    $canvas->Tk::bind ('<Configure>' => [\&Configure, $this]);
    $this->Tk::bind ('<Enter>' => [\&Enter, $this]);
    $this->Tk::bind ('<Leave>' => [\&Leave, $this]);

    return $this;
}

#---------------------------------------------
sub Configure
{
    my ($canvas, $this) = @_;

    return unless ($this->IsMapped());

    my $height = $this->height();
    my $width = $this->width();
	my $trimcount = TRIMCOUNT();

    $canvas->configure(-scrollregion => [0, 0, $width, $height]);

    unless (defined ($this->{'m_TrimList'})) {
        my $highColor = $this->Darken ($this->cget ('-background'), 150);
        my $lowColor = $this->Darken ($this->cget ('-background'), 60);

		my $scale = $this->{_size} / 2 * 3;
        for (my $l_Index = 0; $l_Index < $trimcount; ++$l_Index) {
            push (@{$this->{'m_TrimList'}},
                $canvas->create ('line', $scale, 0, 0, $scale, '-fill' => $highColor),
                $canvas->create ('line', $scale, 1, 1, $scale, '-fill' => $lowColor),
              );
		}
	}

    for (my $l_Index = 0; $l_Index <= $#{$this->{'m_TrimList'}}; $l_Index += 2) {
        my ($l_Light, $l_Dark) = @{$this->{'m_TrimList'}} [$l_Index .. ($l_Index + 1)];
        my $l_Divisor = (($l_Index + 2) / 2) - 1;

        $canvas->coords($l_Light,
            $width, ($height / ($trimcount + 1)) * $l_Divisor,
            ($width / ($trimcount + 1)) * $l_Divisor, $height,
          );

        $canvas->coords($l_Dark,
            $width, (($height / ($trimcount + 1)) * $l_Divisor) + 2,
            ($width / (($trimcount + 1)) * $l_Divisor) + 2, $height,
          );
	}
}

#---------------------------------------------
sub Enter
{
    $_[0]->{'m_Cursor'} = $_[0]->cget ('-cursor');
    $_[0]->configure ('-cursor' => ($^O =~ /^(MSWin32|DOS)$/ ? 'size_nw_se' : 'bottom_right_corner'));
}

#---------------------------------------------
sub Leave
{
    $_[0]->configure ('-cursor' => $_[0]->{'m_Cursor'} || 'arrow');
}

#---------------------------------------------
sub Press
{
    $_[1]->{'-deltax'} = $_[1]->pointerx();
    $_[1]->{'-deltay'} = $_[1]->pointery();
}

#---------------------------------------------
sub Resize
{
    my @geometry = split (/[+x]/, $_[1]->toplevel()->geometry());

	# Take care that we stay in the visible area
	my $x = $_[1]->pointerx();
	if ($x < 0) {
		$geometry [0] = $_[1]->{'-deltax'};
	}
	else {
	    $geometry [0] += ($x - $_[1]->{'-deltax'});
	    $_[1]->{'-deltax'} = $x;
	}
	
	my $y = $_[1]->pointery();
	if ($y < 0) {
		$geometry [1] = $_[1]->{'-deltay'};
	}
	else {
	    $geometry [1] += ($y - $_[1]->{'-deltay'});
	    $_[1]->{'-deltay'} = $y;
	}

	# Force absolute min-sizes of the steered window
	$geometry [0] = MINSIZE_X() if $geometry [0] < MINSIZE_X();
	$geometry [1] = MINSIZE_Y() if $geometry [1] < MINSIZE_Y();

	# Set the new geometry
    $_[1]->toplevel()->geometry($geometry [0] . 'x' . $geometry [1]);
}

1;

########################################################################
# For changing some aspects here we clone the TK-Tree Widget
########################################################################
package Tk::PopUpSelectBox::SelectTree;

use vars qw($VERSION);
$VERSION = '0.01';

use Tk qw(Ev);
use Tk::Derived;
use Tk::HList;
use base  qw(Tk::Derived Tk::HList);

#---------------------------------------------
Tk::Widget->Construct ('SelectTree');

my $minus_data = <<'minus_data_EOP';
	/* XPM */
	static char *xpm[] = {
	"13 13 2 1",
	". c none",
	"X c black",
	".............",
	".............",
	".............",
	"XXXXXXXXXXXXX",
	".XX.......XX.",
	"..XX.....XX..",
	"...XX...XX...",
	"....XX.XX....",
	".....XXX.....",
	"......X......",
	".............",
	".............",
	".............",
};
minus_data_EOP

my $minus_arm_data = <<'minus_arm_data_EOP';
	/* XPM */
	static char *xpm[] = {
	"13 13 3 1",
	". c none",
	"X c black",
	"a c red",
	".............",
	".............",
	".............",
	"XXXXXXXXXXXXX",
	".XXXXXXXXXXX.",
	"..XXXXXXXXX..",
	"...XXXXXXX...",
	"....XXXXX....",
	".....XXX.....",
	"......X......",
	".............",
	".............",
	".............",
	};
minus_arm_data_EOP

my $plus_data = <<'plus_data_EOP';
	/* XPM */
	static char *xpm[] = {
	"13 13 2 1",
	". c none",
	"X c black",
	"...X.........",
	"...XX........",
	"...XXX.......",
	"...X.XX......",
	"...X..XX.....",
	"...X...XX....",
	"...X....XX...",
	"...X...XX....",
	"...X..XX.....",
	"...X.XX......",
	"...XXX.......",
	"...XX........",
	"...X.........",
	};
plus_data_EOP

my $plus_arm_data = <<'plus_arm_data_EOP';
	/* XPM */
	static char *xpm[] = {
	"13 13 3 1",
	". c none",
	"X c black",
	"a c red",
	"...X.........",
	"...XX........",
	"...XXX.......",
	"...XXXX......",
	"...XXXXX.....",
	"...XXXXXX....",
	"...XXXXXXX...",
	"...XXXXXX....",
	"...XXXXX.....",
	"...XXXX......",
	"...XXX.......",
	"...XX........",
	"...X.........",
	};
plus_arm_data_EOP

my %indicators;
my %indicators_data = ('minus', $minus_data, 'minusarm', $minus_arm_data,
					   'plus', $plus_data, 'plusarm', $plus_arm_data);

#---------------------------------------------
sub ClassInit {
	my ($class, $window) = (@_);
	$class->SUPER::ClassInit($window);

	# Note these keyboard-Keys are only usable, if the widget gets 'focus'
	$window->bind($class, '<ButtonRelease-1>', sub { # print "DBG: reached function [BR-Release] with >@_<, called by >", caller, "<\n";
													  my $this = shift;
													  my $path = ($this->infoSelection)[0];
													  if (defined $path and $this->infoChildren($path)) {
														  my $mode = $this->getmode($path);
														  if ($mode eq 'open') { $this->open($path) }
														  elsif ($mode eq 'close') { $this->close($path) }
													  }
													}
	);

	# Auto-Select entries while hovering in the list/tree
    $window->bind($class, '<Motion>', sub {    my $this = shift;
												my $e = $this->XEvent;
												my $y = $e->y;
												my $inx = $this->nearest($y);
												if (defined $inx) {
													$this->anchorClear();
													$this->selectionClear();
													$this->selectionSet($inx);
												}
										    }
	);

 	# Keyboard selection
	$window->bind($class,'<KeyPress>', ['KeyPress', Ev('A')]);
}

#---------------------------------------------
sub Populate
{
	my ($this, $args) = @_;

	$this->SUPER::Populate($args);

	$this->ConfigSpecs(
        -ignoreinvoke => ['PASSIVE',  'ignoreInvoke', 'IgnoreInvoke', 0],
        -opencmd      => ['CALLBACK', 'openCmd',      'OpenCmd', 'OpenCmd' ],
        -indicatorcmd => ['CALLBACK', 'indicatorCmd', 'IndicatorCmd', 'IndicatorCmd'],
        -closecmd     => ['CALLBACK', 'closeCmd',     'CloseCmd', 'CloseCmd'],
        -indicator    => ['SELF', 'indicator', 'Indicator', 1],
        -indent       => ['SELF', 'indent', 'Indent', 15],
        -width        => ['SELF', 'width', 'Width', 20],
        -itemtype     => ['SELF', 'itemtype', 'Itemtype', 'imagetext'],
		-foreground   => ['SELF'],
	);

	# preset indicator images
	foreach (qw(plus plusarm minus minusarm)) {
    	$indicators{$_} = $this->Pixmap(-data => $indicators_data{$_});
	}
}

#---------------------------------------------
sub autosetmode
{
	shift->setmode();
}

#---------------------------------------------
sub IndicatorCmd
{
	my ($this, $ent, $event) = @_;

	my $mode = $this->getmode($ent);

	if ($event eq '<Arm>') {
		if ($mode eq 'open') {
			$this->_indicator_image($ent, 'plusarm');
		}
		else {
			$this->_indicator_image($ent, 'minusarm');
		}
	}
	elsif ($event eq '<Disarm>') {
		if ($mode eq 'open') {
			$this->_indicator_image($ent, 'plus');
		}
		else {
			$this->_indicator_image($ent, 'minus');
		}
	}
	elsif ($event eq '<Activate>') {
		$this->Activate($ent, $mode);
		$this->Callback(-browsecmd => $ent);
	}
}

#---------------------------------------------
sub close
{
	my ($this, $ent) = @_;
	my $mode = $this->getmode($ent);
	$this->Activate($ent, $mode) if ($mode eq 'close');
}

#---------------------------------------------
sub open
{
	my ($this, $ent) = @_;
	my $mode = $this->getmode($ent);
	$this->Activate($ent, $mode) if ($mode eq 'open');
}

#---------------------------------------------
sub getmode
{
	my ($this, $ent) = @_;

	return ('none') unless $this->indicatorExists($ent);

	my $img = $this->_indicator_image($ent);
	return ('open') if ($img eq 'plus' || $img eq 'plusarm');
	return ('close');
}

#---------------------------------------------
sub setmode
{
	my ($this, $ent, $mode) = @_;
	unless (defined $mode) {
		$mode = 'none';
		my @args;
		push(@args,$ent) if defined $ent;
		my @children = $this->infoChildren(@args);
		if (@children) {
			$mode = 'close';
			foreach my $c (@children) {
				$mode = 'open' if $this->infoHidden($c);
				$this->setmode($c);
			}
		}
	}

	if (defined $ent) {
		if ($mode eq 'open') {
			$this->_indicator_image($ent, 'plus');
		}
		elsif ($mode eq 'close') {
			$this->_indicator_image($ent, 'minus');
		}
		elsif ($mode eq 'none') {
			$this->_indicator_image($ent, undef);
		}
	}
}

#---------------------------------------------
sub Activate
{
	my ($this, $ent, $mode) = @_;
	if ($mode eq 'open') {
		$this->Callback(-opencmd => $ent);
		$this->_indicator_image($ent, 'minus');
	}
	elsif ($mode eq 'close') {
		$this->Callback(-closecmd => $ent);
		$this->_indicator_image($ent, 'plus');
	}
	else {
	}
}

#---------------------------------------------
sub OpenCmd
{
	my ($this, $ent) = @_;
	# The default action
	foreach my $kid ($this->infoChildren($ent)) {
		$this->show(-entry => $kid);
	}
}

#---------------------------------------------
sub CloseCmd
{
	my ($this, $ent) = @_;

	# The default action
	foreach my $kid ($this->infoChildren($ent)) {
		$this->hide(-entry => $kid);
	}
}

#---------------------------------------------
sub Command
{
	my ($this, $ent) = @_;

	return if $this->{Configure}{-ignoreInvoke};

	$this->Activate($ent, $this->getmode($ent)) if $this->indicatorExists($ent);
}

#---------------------------------------------
sub _indicator_image
{
	my ($this, $ent, $image) = @_;
	my $data = $this->privateData();
	if (@_ > 2) {
		if (defined $image) {
			$this->indicatorCreate($ent, -itemtype => 'image') unless $this->indicatorExists($ent);
			$data->{$ent} = $image;
			#$this->indicatorConfigure($ent, -image => $this->Getimage($image));
			$this->indicatorConfigure($ent, -image => $indicators{$image});
		}
		else {
			$this->indicatorDelete($ent) if $this->indicatorExists($ent);
			delete $data->{$ent};
		}
	}
	return $data->{$ent};
}



#----------------------------------------------------------------------
#               Accelerator key bindings
#----------------------------------------------------------------------
# inspired by tkIconList_KeyPress --
#
# Gets called when user enters an arbitrary key in the listbox.
#
sub KeyPress
{
    my ($w, $key) = @_;

    $w->{'_HLAccel'} .= $key;
    $w->Goto($w->{'_HLAccel'});
    eval { $w->afterCancel($w->{'_HLAccel_afterid'}) };
    $w->{'_HLAccel_afterid'} = $w->after(500, ['Reset', $w]);
}
sub Goto
{
    my ($w, $text) = @_;
	
	# Locals
	my (@children, $pattern, $selitem);

    return if (not defined $text or $text eq '');
	@children = $w->collectChildren();

	$pattern = qr/^(?i)$text/;
	foreach (@children) {
		$entry = $w->itemCget($_, 0, '-text');
		if ($entry =~ $pattern) {
			$selitem = $_;
			last
		}
	}
    if ($selitem) {
		$w->selectionClear();
		$w->selectionSet($selitem);
		$w->anchorSet($selitem);
		$w->see($selitem);
 		$w->Callback(-browsecmd =>$selitem);
   }
}
sub collectChildren
{
    my ($w, $item) = @_;

	# Locals
	my (@children, @grandchilds);
	@children = $w->infoChildren($item);
	foreach (@children) {
		push @grandchilds, $w->collectChildren($_)
	}
	return @children, @grandchilds;
}

sub Reset {
    my $w = shift;
    undef $w->{'_HLAccel'};
}

1;



########################################################################
# Here we start with the Real Widget
########################################################################


package Tk::PopUpSelectBox;

##############################################
### Use
##############################################
use Storable qw(freeze);
use Tk;

use Tk::Button;
use Tk::Tree;

use Carp qw(:DEFAULT cluck);

use vars qw ($VERSION);
use base qw(Tk::Frame);
use strict;

$VERSION = '1.0';


#---------------------------------------------
use constant DEFAULT_SEPARATOR	=> '.';
use constant MIN_BLOCK_TIME	=> '250'; # in ms


#---------------------------------------------
Tk::Widget->Construct ('PopUpSelectBox');


my $cbx_arrow = << 'cbx_arrow_EOP';
	/* XPM */
	static char *cbxarrow[] = {
	/* columns rows colors chars-per-pixel */
	"11 14 2 1",
	"  c black",
	". c gray100",
	/* pixels */
	"...........",
	"....   ....",
	"....   ....",
	"....   ....",
	"....   ....",
	"....   ....",
	".         .",
	"..       ..",
	"...     ...",
	"....   ....",
	"..... .....",
	"...........",
	".         .",
	".         ."
	};
cbx_arrow_EOP

my $arrowdownwin = << 'arrowdownwin_EOP';
	/* XPM */
	static char *arrowdownwin[] = {
	/* columns rows colors chars-per-pixel */
	"9 13 2 1",
	"  c black",
	". c gray100",
	/* pixels */
	".........",
	".........",
	".........",
	".........",
	".........",
	"..     ..",
	"...   ...",
	".... ....",
	".........",
	".........",
	".........",
	".........",
	"........."
	};
arrowdownwin_EOP


#---------------------------------------------
sub Populate {
	# Parameters
	my ($this, $args) = @_;

	# Locals
	my ($var, $bttn, $tl, $list, $separator, %defaults);
	local $_;

	# check for special options
	$this->{'_ignoreExisting'} = delete $args->{-ignoreExisting};
	$separator = delete $args->{-separator} || DEFAULT_SEPARATOR();
	$this->{separator} = $separator;

	# Create a Closure for saving the current value
	$var = delete $args->{-variable};
	unless ($var) {
		my $gen = undef;
		$var = \$gen;
	}
	
	# Createa the widget
    $this->Tk::Frame::Populate($args);

    # store the os-style
    $this->{_style} = delete $args->{-style} || $Tk::platform;

    #my $bitmap = '@' . Tk->findINC($this->{_style} eq 'MSWin32' ? 'arrowdownwin.xbm' : 'cbxarrow.xbm');
    my $image = $this->Pixmap(-data => ($this->{_style} eq 'MSWin32' ? $arrowdownwin : $cbx_arrow));
	$bttn = $this->Button(
#			-bitmap => $bitmap,
			-image => $image,
			-relief => 'flat',
			-bd => 0,
			-padx => -1,
			-pady => -1,
	)->pack(-side => 'right', -anchor => 's');
    $this->Advertise('arrow' => $bttn);

    # popup shell for listbox with values.
    $tl = $this->Toplevel(-bd => 2, -relief => ($this->{_style} eq 'MSWin32' ? "solid" : "raised"));
    $tl->overrideredirect(1);
	$tl->minsize(30,30);
    $tl->withdraw;
	#my $fr = $tl->Frame(-relief => 'sunken', -borderwidth => 2)->pack(qw(-fill both -expand 1));
    #my $sl = $fr->Scrolled('SelectTree',
    my $sl = $tl->Scrolled('SelectTree',
		-scrollbars => 'e',
		-separator => $separator,
		-borderwidth => 0, -relief => 'flat',
		-selectmode => 'single',
		-command => sub {
							my $path = shift;
							my $tree = $this->{_tree};	
							my $mode = $tree->getmode($path);
							# Eventually open or close te child-tree
							if ($mode eq 'open') {
								$tree->open($path);
							}
							elsif ($mode eq 'close') {
								$tree->close($path);
							}
						},
 	)->pack(-expand => 1, -fill => 'both');
	#
    if ($this->{_style} eq 'MSWin32' and $Tk::platform eq 'MSWin32') {
		$sl->configure(-bg => 'SystemWindow', -relief => "flat");
    }
	my $tree = $this->{_tree} = $sl->Subwidget('scrolled');
	$tree->configure();

	# Position the Resizer in the lower right area of the scroller
	$sl->Subwidget('ysbslice')->ResizeCBox(-size => 15)->pack(
		-side => 'bottom', -anchor => 'se', -before => $sl->Subwidget('yscrollbar'), 
	);
	# Brush up the scroller
 	$sl->Subwidget('yscrollbar')->configure(-relief => 'flat', -width => 10, -borderwidth => 2);

	# Propagate the internal structure	
    $this->Advertise('window' => $tl);

    # other initializations
    # set bind tags
    $this->bindtags([$this, 'Tk::PopUpSelectBox', $this->toplevel, 'all']);

    # bindings for the button and entry
    $bttn->bind('<1>',[$this, 'button_down']);
    $bttn->toplevel->bind('<ButtonRelease-1>', [$this, 'button_restore']);
    $bttn->bind('<space>', [$this, 'space']);

    # bindings for listbox
    $tree->bind('<ButtonRelease-1>', sub { $this->list_selected(@_) });
    $tree->bind('<Escape>' => [$this, 'list_close']);
    $tree->bind('<Return>' => [$this, 'return', $tree]);
    $tree->bind('<Enter>' => sub { $this->{'_inside'} = 1 });
    $tree->bind('<Leave>' => sub { $this->{'_inside'} = 0 });

    # allow click outside the popped up listbox to pop it down.
    $this->bind('<1>','button_down');

    $this->{'_popped'} = 0;
    $this->Delegates(get => $bttn, DEFAULT => $bttn);

	# Setup DEFAULT Configs
	%defaults = (
    	-selectborderwidth	=> [$tree, 'selectBorderwidth', 'SelectBorderwidth', '1'],
    	-selectbackground	=> [$tree, 'selectBackground', 'SelectBackground', '#ececec'],
    	-selectforeground	=> [$tree, 'selectForeground', 'SelectForeground', 'Black'],
    	-activebackground	=> [$bttn, 'activeBackground', 'ActiveBackground', '#ececec'],
    	-activeforeground	=> [$bttn, 'activeForeground', 'ActiveForeground', 'Black'],
    	-takefocus			=> ['SELF', 'takefocus', 'Takefocus', 1],
    	-highlightthickness	=> ['SELF', 'highlightThickness', 'HighlightThickness', 1],
    	-borderwidth		=> [['SELF', 'PASSIVE'], 'borderwidth', 'BorderWidth', 2],
    	-relief				=> [['SELF', 'PASSIVE'], 'relief', 'Relief', 'raised'],
    	-anchor				=> [['SELF', 'PASSIVE'], 'anchor', 'Anchor', 'w'],
    	#-ignoreExisting		=> [['SELF', 'PASSIVE'], 'ignoreExisting', 'IgnoreExisting', '0'],
    	-listbackground		=> [{-background => $tree}, 'background', 'Background', '#ececec'],
    	-font				=> ['DESCENDANTS', 'font', 'Font', 'Helvetica 12 bold'],
    	-variable 			=> ['PASSIVE', 'variable', 'Variable', $var],
    	-activate 			=> ['PASSIVE', 'activate', 'Activate', 1],
    	-separator 			=> ['PASSIVE', 'separator', 'Separator', $separator],
    	-options 			=> ['METHOD',  undef, undef, undef],
    	-command 			=> ['CALLBACK',undef,undef,undef],
    	-validatecommand	=> ['PASSIVE', 'validatecommand', 'ValidateCommand', sub {0}],

        -listwidth			=> [qw/PASSIVE  listWidth   ListWidth/,   undef],
        -listmaxheight		=> [qw/PASSIVE listMaxHeight ListMaxHeight 0/],
        -listcmd			=> [qw/CALLBACK listCmd     ListCmd/,     undef],
        -autolistwidth		=> [qw/PASSIVE autoListWidth AutoListWidth/, 1],
        -autolimitheight	=> [qw/PASSIVE autoLimitHeight AutoLimitHeight 1/],
        -state				=> [qw/METHOD   state       State         normal/],
    	-listheight 		=> ['METHOD',  undef, undef, undef],
        -image 				=> [ {-image => $bttn}, qw/arrowImage ArrowImage/, undef],
		-arrowimage			=> '-image',
		-rows				=> '-listheight',
		-buttontakefocus	=> [{-takefocus => $bttn}, 'buttonTakefocus', 'ButtonTakefocus', 1],
		-bitmap 			=> [{-bitmap => $bttn}, 'bitmap', 'Bitmap', 'question'],
		DEFAULT 			=> [$bttn]

	);	
	$this->ConfigSpecs(%defaults);

	$this->bind('<ButtonPress-3>'   => [$this => 'b3prs', Ev('x'), Ev('y')]);
	$this->bind('<ButtonRelease-3>' => [$this => 'b3rls', Ev('X'), Ev('Y')]);

	# Reset internal storage
	$this->{'_item_count'} = 0;
	$this->{'_item_width'} = 0;
	$this->{'_item_table'} = {};

}
	
#---------------------------------------------
sub listheight
{
    #print "DBG: reached function [listheight] with >@_<, called by >", caller, "<\n";
	my ($this, $height) = @_;

	my $oldh = $this->{_tree}->cget('-height');
   	$this->{_tree}->configure('-height' => $height + 1) if $height;
	return ($oldh);
}

#---------------------------------------------
sub state {
    my $this = shift;

    my $button = $this->Subwidget('arrow');

    if (@_) {
        $button->configure(-state => shift);
    }
	else {
        return $button->cget('-state');
    }
}

#---------------------------------------------
sub space
{
    my $this = shift;

	$this->button_down;
	$this->{'_savefocus'} = $this->focusCurrent;
	$this->Subwidget('list')->focus;
}

#---------------------------------------------
sub button_down
{
    #print "DBG: reached function [button_down] with >@_<, called by >", caller, "<\n";
    my $this = shift;
    return if $this->cget('-state') eq 'disabled';

    if ($this->{'_popped'}) {
		$this->list_popdown;
		$this->{'_button_restore'} = 0;
    } else {
		$this->popup;
		$this->{'_button_restore'} = 1;
    }
}

#---------------------------------------------
# triggered the listbox after selection
sub list_selected
{
	#print "DBG: reached function [list_selected] with >@_<, called by >", caller, "<\n";
    my $this = shift;
	return if $this->{'_popping'};
	
	if ($this->{'_inside'}) {
    	my $tree = $this->{_tree};
		my $path = ($tree->infoSelection)[0];

		# Do not take this click as a real selection, if it is a subhierarchy
		my @children = $tree->infoChildren($path);	
		if (@children) {
			#print "DBG: variable [\@children] = >@children<\n";
			my ($len, $child); $len = 0;
			foreach (@children) {
				if (length($_) > $len) {
					$child = $_;
					$len = length($_);
				}
			}
			# Show the last child if there is enough space in the list
			my $tmp1 = $tree->cget('-height');
			if (@children > $tree->cget('-height')) {
				$tree->see($children[0]);
			}
			else {
				$tree->see($children[-1]);
			}

			my $width = $this->fontMeasure($this->cget('-font'), $child);
			if ($this->cget('-autolistwidth') and defined $path and $path ne "") {
				my $geom = $tree->toplevel->geometry;
				my ($w, $h, $x, $y) = $geom =~ /(\d+)x(\d+)([+-]\d+)([+-]\d+)/;
				$w = int($width * 1.5) + ($tree->indicatorSize($path))[0];
				
				# if listbox is too far right, pull it back to the left
				$x = $this->vrootwidth - $w - 5 if (($x + $w) > $this->vrootwidth);

				# if listbox is too far left, pull it back to the right
				$x = 0 if $x < 0;
				$x = '+' . $x if ($x =~ /^\d/o);
				#print "DBG: variable [\$x] = >$x<\n";
				$tree->toplevel->geometry($w . 'x' . $h . $x . $y);
				#print "DBG: variable [\@geometry] = >@geometry<\n";
			}
			return;
		}

		my $value = $tree->infoData($path);
		$this->set_option($tree->itemCget($path, 0, '-text'), $value, $path);
	
		# ....
		$this->list_popdown();
	}
	#print "DBG: end function [list_selected]\n";
}
#---------------------------------------------
# close the listbox after restoring the button
sub list_release
{
    #print "DBG: reached function [list_release] with >@_<, called by >", caller, "<\n";
	my ($this, $x, $y) = @_;

	unless ($this->{'_inside'}) {
		$this->button_restore;
		$this->list_close($x, $y);
	}
}

#---------------------------------------------
# close the listbox after clearing selection
sub list_close
{
    #print "DBG: reached function [list_close] with >@_<, called by >", caller, "<\n";
    my $this = shift;

    my $tree = $this->{_tree};
    $tree->selectionClear();
    $this->list_popdown;
}

#---------------------------------------------
# pop down the listbox
sub list_popdown
{
    #print "DBG: reached function [list_popdown] with >@_<, called by >", caller, "<\n";
    my $this = shift;

    if ($this->{'_savefocus'} && Tk::Exists($this->{'_savefocus'})) {
		$this->{'_savefocus'}->focus;
		delete $this->{'_savefocus'};
    }
    if ($this->{'_popped'}) {
		my $c = $this->Subwidget('window');
		$this->{'_geometry'} = $c->geometry;
		$c->withdraw;
		$this->grabRelease;
		if (ref $this->{'_grabinfo'} eq 'CODE') {
			$this->{'_grabinfo'}->();
			delete $this->{'_grabinfo'};
		}
		# Restore the focus
	    $this->{'_focus'}->focus() if $this->{'_focus'};
		undef $this->{'_focus'};

		$this->{'_popped'} = 0;
    }
	$this->button_restore;
}

#---------------------------------------------
# Pressed ENTER bttn
sub return
{
    #print "DBG: reached function [return] with >@_<, called by >", caller, "<\n";
    my $this = shift;

    my $tree = $this->{_tree};
	my $path = $tree->infoSelection();
	my $value = $tree->infoData($path);
	$this->set_option($tree->itemCget($path, 0, '-text'), $value, $path);

	$this->list_popdown();
}

#---------------------------------------------
# This hack is to prevent the ugliness of the arrow being depressed.
sub button_restore
{
    #print "DBG: reached function [button_restore] with >@_<, called by >", caller, "<\n";
    my $this = shift;

    my $b = $this->Subwidget('arrow');
    if ($this->{'_button_restore'}) {
		$b->butUp;
		delete $this->{'_button_restore'};
    }
}

#---------------------------------------------
# displaying the selection list
sub popup
{
    #print "DBG: reached function [popup] with >@_<, called by >", caller, "<\n";
    my $this = shift;

    unless ($this->{'_popping'} or $this->{'_popped'}) {
		$this->{'_popping'} = 1;

		$this->Callback(-listcmd => $this);
		my $c = $this->Subwidget('window');
		my $a = $this->Subwidget('arrow');

		my $y1 = $a->rooty + $a->height / 2;
		my $bd = $c->cget(-bd) + $c->cget('-highlightthickness');
		# using the real listbox reqheight rather than the
		# container frame one, which does not change after resizing the
		# listbox
		my $ht = $this->{_tree}->reqheight + 4 * $bd + 2;
		$ht = $this->{_tree}->height + 4 * $bd + 2 if $this->{'_geometry'};
		$ht = 50 if $ht < 20;
		#print "DBG: variable [\$ht] = >$ht<\n";

		my $x1 = $a->rootx;
		
		my ($width, $x2);
		if (defined $this->cget('-listwidth')) {
	    	$width = $this->cget('-listwidth');
	    	$x2 = $x1 + $width;
		}
		else {
	    	$x2 = $a->rootx + $a->width;
	    	$width = $x2 - $x1;
		}
    	my $rw = $c->reqwidth;
    	if ($rw < $width) {
    	    $rw = $width
    	}
		else {
    	    if ($rw > $width * 3) {
    			$rw = $width * 3;
    	    }
    	    if ($rw > $this->vrootwidth) {
    			$rw = $this->vrootwidth;
    	    }
    	}
    	$width = $rw;

		# if listbox is too far right, pull it back to the left
		$x1 = $this->vrootwidth - $width if $x2 > $this->vrootwidth;

		# if listbox is too far left, pull it back to the right
		$x1 = 0 if $x1 < 0;


		# if listbox is below bottom of screen, pull it up.
		# check the Win32 taskbar, if possible
		my $rootheight;
		if ($Tk::platform eq 'MSWin32' and $^O eq 'MSWin32') {
	    	eval {
			require Win32Util; # XXX should not use a non-CPAN widget
			$rootheight = (Win32Util::screen_region($this))[3];
	    	};
		}
		$rootheight = $this->vrootheight unless defined $rootheight;


		my $y2 = $y1 + $ht;
		if ($y2 > $rootheight) {
	    	$y1 = $y1 - $ht - ($a->height - 5);
		}
		$this->after(MIN_BLOCK_TIME(), sub { $this->{'_popping'} = 0 });

		$y1 = 0 if $y1 < 0;
		if ($this->{'_geometry'}) {
			my @geometry = $this->{'_geometry'} =~ /(\d+)x(\d+)([+-]\d+)([+-]\d+)/;
			$rw = $geometry[0]; $ht = $geometry[1];
		}

		#print "DBG: variable [\$rw] = >$rw< [\$ht] = >$ht< [\$x1] = >$x1< [\$y1] = >$y1< \n";
		$c->geometry(sprintf('%dx%d+%d+%d', $rw, $ht, $x1, $y1));
		$c->deiconify;
		$c->raise;
		$this->{'_popped'} = 1;

		# highlight current selection
		# TODO - needed ?
		
		$c->configure(-cursor => 'arrow');
		$this->{'_grabinfo'} = $this->grabSave;
		$this->grabGlobal; #block all in system
		#$this->grab; # block only other windows in app
		
		# move the focus into the list to ease keyboard usage
	    $this->{'_focus'} = $this->focusCurrent();
		$this->{_tree}->focus();
    }
    #print "DBG: end function [popup]\n";
}
# Screen-move methods.

sub b3prs {
    my ($self, $x, $y) = @_;
    $self->{ofx} = $x;
    $self->{ofy} = $y;
} # end b3prs

sub b3rls {
    my($self, $X, $Y) = @_;
    $X -= $self->{ofx};
    $Y -= $self->{ofy};
    $self->geometry("+${X}+${Y}");
} # end b3rls

#---------------------------------------------
{
my ($List, $Separator, $FingerPrint); # Use global instead of func-arg for speed
	sub add_options
	{
		#print "DBG: reached function [add_options] with >@_<, called by >", caller, "<\n";

		# Parameters
    	my ($this, @args) = @_;

		# Locals
		my ($old, $var, $items, $test);

		#-----------------------------------------------------------------------------
		if (ref $args[0] eq 'ARRAY') {
			$items = $args[0];
		}
		elsif (@args) {
			$items = \@args;
		}

		#-----------------------------------------------------------------------------
		# Check if we already prepared exactly the same tree
		$test = freeze($items);
		return if $FingerPrint and $FingerPrint eq $test and $this->{'_item_count'} > 0;
		$FingerPrint = $test;
		#print "building new tree...\n";

		# Fake a busy state 
		my $a = $this->Subwidget('arrow');
		my $old_cursor = $a->cget('-cursor'); $a->configure(-cursor => 'watch');
		$a->update();

		$var = $this->cget('-variable');
		# Store old selection
		$old = $$var;

		# Clear & Fill the list with new values
		$List = $this->{_tree};
		$Separator = $List->cget('-separator');

		$List->delete('all');
		$this->{'_item_count'} = 0;
		$this->{'_item_width'} = 0;
		$this->{'_item_table'} = {};

		
		$this->_add_option('', $items);
		#-----------------------------------------------------------------------------
		#print "DBG: variable [$this->{'_item_count'}] = >", $this->{'_item_count'}, "<\n";		
		
    	$this->limitheight() if $this->cget('-autolimitheight'); 
		$this->updateListWidth() if $this->cget('-autolistwidth');

		$a->configure(-cursor => $old_cursor);
		$a->update();
	}

	#-----------------------------------------------------------------------------
	sub _add_option
	{
    	#print "DBG: reached function [_add_option] with >@_<, called by >", caller, "<\n";
		# Parameters
    	my ($this, $parent, $items) = @_;

		my ($item, $path, $label, $value, $width);

		foreach $item (@$items) {
			if (ref $item) {
				($label, $value) = @$item
			}
			else {
				$label = $value = $item
			}

			if (ref $label) {
				if ($parent eq '') {
					$path = $label->[0];
				}
				else {
					$path = $parent . $Separator . $label->[0];
				}
				$List->add($path, -text => $label->[0], -data => undef) unless $List->infoExists($path);
				$this->_add_option($path, $label->[1]);
				$List->Activate($path, 'close');
				$this->{'_item_count'}++ unless $parent; #count only toplevel entries
			}
			else {
				if ($parent eq '') {
					$path = $label;
				}
				else {
					$path = $parent . $Separator . $label;
				}
				if ($List->infoExists($path)) {
					croak "Entry already exists!\n" unless $this->{'_ignoreExisting'}; 
				}
				else {
					$List->add($path, -text => $label, -data => $value);
					#print "DBG: variable [\$path] = >$path< [\$label] = >$label< [\$value] = >$value<\n";
					# Update internal storages
					$this->{'_item_count'}++ unless $parent; #count only toplevel entries
					$this->{'_item_table'}{$path} = $value;
					$width = $this->fontMeasure($this->cget('-font'), $label);
					$this->{'_item_width'} = $width if $width > $this->{'_item_width'};
				}
			}
		}
		#print "end function [_add_option]\n";
	}
}
#-----------------------------------------------------------------------------
sub limitheight
{
    my $this = shift;

    my $listheight = shift || $this->{'_item_count'};
	#print "DBG: variable 1 [\$listheight] = >$listheight<\n";
	my $listmaxheight = $this->cget('-listmaxheight');
    $listheight = $this->cget('-listmaxheight') if $listmaxheight > 0 and $listheight > $listmaxheight;
    $this->configure(-listheight => $listheight) if ($listheight > 0);
}

#-----------------------------------------------------------------------------
sub updateListWidth
{
    #print "DBG: reached function [updateListWidth] with >@_<, called by >", caller, "<\n";
    my $this = shift;

	my $width = $this->{'_item_width'};
	#print "DBG: variable [\$width] = >$width<\n";
    if ($this->{'_item_width'} > 10) { # be sane
		$this->configure(-listwidth => $this->{'_item_width'} + 5); # + XXX for scrollbar
    }
}

#---------------------------------------------
sub set_option
{
    #print "DBG: reached function [set_option] with >@_<, called by >", caller, "<\n";
	# Parameters
	my ($this, $label, $value, $full_label) = @_;
	# Locals
	my ($failed, $validatecommand, $variable, $textvariable, $old_label, $old_value);
	
	# Some presettings
	$value = $label if @_ == 2;
	$full_label = $label unless $full_label;
	return if $full_label eq "";

	$validatecommand = $this->cget('-validatecommand');
	$textvariable = $this->cget('-variable');
	$variable = $this->cget('-variable');

	$old_value = $variable ? $$variable : $this->{OldValue};
	$old_label = $$textvariable;
	
	# Perform validate operation, if available
	do { $failed = &$validatecommand ($this, $value, $label, $full_label, $old_value, $old_label) } if $validatecommand;
	
	#Do the changes
	unless ($failed) {
		$$variable = $value if $variable;
		$this->{OldValue} = $value;
		$$textvariable = $label;

		my $list = $this->{_tree};
		$list->selectionClear();
		$list->selectionSet($full_label);
		$list->see($full_label);

		# Now invoke the callback
		$this->{CallBackActive} = 1;
		$this->Callback(-command => $value, $label, $full_label);
		delete $this->{CallBackActive};
	}
}

#---------------------------------------------
sub options
{
	my ($this, $opts) = @_;
	if (@_ > 1) {
		if ($this->{CallBackActive}) {
			cluck "\nTk::PopupSelectionBox Error: Found an illegal recursion loop: from Callback() to options() which is not allowed!\nAuto-shutting-down now, please let the developer fix this!";
			kill 9, $$;
		}
		$this->add_options($opts);
	}
	else {
		#return $this->_cget('-options');
	}

}
#---------------------------------------------
sub itemtable
{
	my $this = shift;
	my %itemtable = $this->{'_item_table'} ? %{$this->{'_item_table'}} : ();

	return wantarray ? %itemtable : \%itemtable;
}

########################################################################
1;
__END__


=head1 NAME

Tk::PopUpSelectBox - A new scrolled pop-up selection-widget (with MULTI-level selections) 

=head1 SYNOPSIS

    use Tk;
    use Tk::PopUpSelectBox

    my $current_class;
    my @all_classes = qw(cat dog bird);
    my $demo_xpm;
	
    my $mw = MainWindow->new();
	
    # prepare some graphics
    setup_pixmap();

    # create a demo 
    my $popupselectbox = $mw->PopUpSelectBox (
        -text     => "Class",
        -image    => $demo_xpm, # use this line for personal pics or
        #-bitmap  => '@' . Tk->findINC('cbxarrow.xbm'));
        -command  => \&class_cb,
        -options  => [ @all_classes ],
        -variable => \$current_class, 
		-tearoff  => '1',
		-listmaxheight => 10,
		-activate => '0',
   )->pack;
	
    Tk::MainLoop;
	
    sub class_cb
    {
        print "class_cb called with [@_], \$current_class = >$current_class<\n";
    }
    sub setup_pixmap
    {
        my $cbxarrow_data = <<'cbxarrow_EOP';
	/* XPM */
	static char *cbxarrow[] = {
	"11 14 2 1",
	". c none",
	"  c black",
	"...........",
	"....   ....",
	"....   ....",
	"....   ....",
	"....   ....",
	"....   ....",
	".         .",
	"..       ..",
	"...     ...",
	"....   ....",
	"..... .....",
	"...........",
	".         .",
	".         ."
	};
cbxarrow_EOP

        $demo_xpm = $mw->Pixmap(-data => $cbxarrow_data);
    }
	

=head1 DESCRIPTION

A new dialog style widget that can replace the custom Optionbox whenever the itemlist is too long.
Useful in applications that want to use a more flexible option menu. 
It's a 1:1 replacement for the custom Optionbox, supporting the same Options / commands.

You can tie a scalar-value to the Optionbox widget, enable/disable it,
assign a callback, that is invoked each time the Optionbox is changed,
as well as set Option-values and configure any of the options
understood by Tk::Frame(s) like -relief, -bg, ... .
(see docs of TK::Optionmenu) for details

=head1 METHODS

=over 4

=item B<set_option()>

'set_option($newvalue)' allows to set/reset the widget methodically,
$newvalue will be aplied to the labeltext (if visible) and the internal
variable regardless if it is a list previously store in options.

NOTE: You should prefer interacting with the widget via a variable.


=item B<add_options()>

'add_options(@newoptions)' allows to enter additonal options that will be
displayed in the pull-down menu list.

You should prefer to use a Configure ('-options' => ...).

NOTE: Unless You specify -activate => 0 for the widget each time you use
add_options the first item will be set to be the current one and any assigned
callback gets called.

=item B<popup()>

'popup()' allows to immediately popup the menu to force the user
to do some selection.

=item B<itemtable()>

'itemtable()' retrieves a list of all current selection items.
Requesting a listcontext retrieves a label/value based hash, retrieving
a scalar retrieves a hash-ref. NOTE the B<-separator> setting for
the hierarchical delimiter wherever applied.

=back


=head1 OPTIONS

=over 4

=item B<-variable>

'-variable' allows to specify a reference to a scalar-value.
Each time the widget changes by user interaction, the variable
is changed too. Every variable change is immediately mapped in the
widget too.


=item B<-command>

'-command' can be used to supply a callback for processing after
each change of the Option value.
This callback receives as parameters 'current' value + label + full-label
for hierarcical (sub)lists. NOTE the B<-separator> setting for
the hierarchical delimiter applied for full-label.

=item B<-image>

'-image' can be used to supply a personal bitmap for the menu-button.
In difference to the original Optionmenu the std. menu-indicator is
switched off, if a graphic/bitmap is used , although it might
be re-enabled manually with a B<'-indicatoron =\> 1'> setting.
If no image and no bitmap is specified the text given with B<'-text'>
or the current selected optiontext is displayed in the button.

=item B<-options>

'-options' expects a reference to a list of options.

plain format: label, label, [ label, value ], [ label, value ], [ label, value ], ...
multi-level selection: The methodology is the same as before:
Whenever instead of a label an Array-reference is found it is suggested as a subitem-list.
It is poosible to mix plain items, items with spec'd values other than the label in any level.
example: label, label, [ label, value ], [[keylabel, \@subopts], undef], [ label, value ],
See the supplied example for further information.

=item B<-activate>

'-activate' expects 0/1 and rules whether the first item applied with -options gets
set 'current'. see NOTE above.

=item B<-listmaxheight> or B<-rows>

'-listmaxheight' defines the height of the selection list. default is 20.

=item B<-separator>

'-separator' defines the separator character that is used for the internal representation
of the tree delimiter. Invoking a callback via set_options the target function get [value,
label & full-hierarchical label]. The f-h-label uses the specified separator. Default is '.'

=item B<-autolistwidth>

'-autolistwidth' expects 0/1 and defines whether the pop'd up list will dynamically adapt its width.

=item B<-autolimitheight>

'-autolistwidth' expects 0/1 and defines whether the pop'd up list will not be heigher than the value
definable via '-listmaxheight'

=item B<-validatecommand>

'-validatecommand' defines a Callback function to evaluate the current selection.
It is invoked with B<the_widget>, B<value>, B<label>, B<full_label>, B<old_value> and B<old_label>.
Returning B<FALSE> will reject the current selection.

=back

Please see the TK:Optionmenu docs for details on all other aspects
of these widgets.


=head1 AUTHORS

Michael Krause, KrauseM_AT_gmx_DOT_net

This code may be distributed under the same conditions as Perl.

V0.02  (C) March 2008

=cut

###
### EOF
###