The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Gimp::UI;

use Gimp ('__');
use Gimp::Fu;
use POSIX qw(locale_h);
my $locale;
BEGIN { $locale = setlocale(LC_NUMERIC); setlocale(LC_NUMERIC, "C"); }
use Gtk2;
BEGIN { setlocale(LC_NUMERIC, $locale); }
use IO::All;
use List::Util qw(min);
use strict;
use warnings;

our (@ISA, $VERSION);
BEGIN {
   no locale;
   $VERSION = "2.31";
   eval {
      require XSLoader;
      XSLoader::load Gimp::UI $VERSION;
   } or do {
      require DynaLoader;
      @ISA = qw(DynaLoader);
      bootstrap Gimp::UI $VERSION;
   };
}

# shows the properties of a glib object
#d# just to debug
sub info {
   my ($idx, $obj) = @_;
   my %seen;
   return if $seen{$idx}++;
   print "\n$idx\n";
   for ($obj->list_properties) {
      printf "%-16s %-24s %-24s %s\n", $_->{name}, $_->{type}, (join ":", @{$_->{flags}}), $_->{descr};
   }
}

@Gimp::UI::Combo::Image::ISA   =qw(Gimp::UI::Combo);
@Gimp::UI::Combo::Layer::ISA   =qw(Gimp::UI::Combo);
@Gimp::UI::Combo::Channel::ISA =qw(Gimp::UI::Combo);
@Gimp::UI::Combo::Drawable::ISA=qw(Gimp::UI::Combo);

package Gimp::UI::Combo;

use Gimp ('__');
our @ISA = 'Gtk2::ComboBox';

sub image_name { $_[0]->get_filename || "Untitled-".${$_[0]}; }

sub Gimp::UI::Combo::Image::_items {
  +{ map { (image_name($_) => $_) } Gimp::Image->list }
}

sub Gimp::UI::Combo::Layer::_items {
  +{
    map {
      my $i = $_;
      map { (image_name($i)."/".$_->get_name => $_) } $i->get_layers;
    } Gimp::Image->list
  }
}

sub Gimp::UI::Combo::Channel::_items {
  +{
    map {
      my $i = $_;
      map { (image_name($i)."/".$_->get_name => $_) } $i->get_channels;
    } Gimp::Image->list
  }
}

sub Gimp::UI::Combo::Drawable::_items {
  +{ %{Gimp::UI::Combo::Channel->_items}, %{Gimp::UI::Combo::Layer->_items} }
}

sub new($) {
  my ($class,$var)=@_;
  my $self = bless $class->SUPER::new_text, $class;
  $self->{GIMPUI_text2scalar} = {};
  $self->reload;
  $self;
}

sub get_active_scalar {
  my $self = shift;
  $self->{GIMPUI_text2scalar}->{$self->get_active_text};
}

sub reload {
  warn __PACKAGE__ . "::reload(@_)" if $Gimp::verbose >= 2;
  my ($self) = @_;
  my $count = keys %{ $self->{GIMPUI_text2scalar} };
  $self->remove_text(0) while $count--;
  my $t2s = $self->_items;
  my @items = keys %$t2s;
  $t2s = { '(none)' => undef } unless @items;
  for my $t (keys %$t2s) { $self->append_text($t); }
  $self->{GIMPUI_text2scalar} = $t2s;
  $self->set_active(0);
}

package Gimp::UI::PreviewSelect;

# Parent widget that handles a generic preview selection.
#
# pure virtual methods (must be implemented by child):
#                  ->get_list
#                  ->get_title
#                  ->get_pixbuf
#TODO: Add preview (or portion of preview) directly to button

use Gtk2::SimpleList;

our @ISA = 'Glib::Object';

Glib::Type->register (
  'Gtk2::Button', __PACKAGE__,
  signals => {},
  properties => [
    Glib::ParamSpec->string(
      'active',
      'Active',
      'The active child',
      '',
      [qw/readable writable/]
    ),
  ],
);

sub SET_PROPERTY {
  my ($self, $pspec, $newval) = @_;
  return unless $pspec->get_name eq 'active';
  $self->{active} = $newval;
  $self->set_label($newval);
}

sub GET_PROPERTY {
  my ($self, $pspec) = @_;
  if ($pspec->get_name eq 'active') {
    return $self->{active};
  }
}

sub INIT_INSTANCE {
  my ($self) = @_;
  $self->signal_connect("clicked", \&preview_dialog);
  my $lbl = new Gtk2::Label $self->get("active");
  $self->add($lbl);
}

sub preview_dialog {
  my ($self) = @_;
  my $w = new Gtk2::Dialog;
  $w->set_title($self->get_title);
  $w->set_default_size(400,300);
  $w->action_area->set_border_width(2);
  $w->action_area->set_homogeneous(0);
  (my $h=new Gtk2::HBox 0,0)->show;
  $w->vbox->pack_start($h,1,1,0);
  (my $s=new Gtk2::ScrolledWindow undef,undef)->show;
  $s->set_policy(-automatic, -automatic);
  $s->set_size_request(200,300);
  $h->pack_start($s,1,1,0);
  my $datalist = new Gtk2::SimpleList (
    'Name' => 'text',
    'Preview' => 'pixbuf',
  );
  for(sort $self->get_list) {
    my $listname = $_;
    my $pixbuf = $self->new_pixbuf($listname);
    push @{$datalist->{data}}, [ $listname, $pixbuf ];
  }
  $datalist->get_selection->set_mode('single');
  $datalist->get_selection->unselect_all;
  $s->add($datalist);
  $datalist->show;
  my $hbbox = new Gtk2::HButtonBox;
  $hbbox->set_spacing(2);
  $w->action_area->pack_end($hbbox,0,0,0);
  show $hbbox;
  my $button = new Gtk2::Button->new_from_stock('gtk-cancel');
  signal_connect $button clicked => sub {hide $w};
  $hbbox->pack_start($button,0,0,0);
  can_default $button 1;
  show $button;
  $button = new Gtk2::Button->new_from_stock('gtk-ok');
  signal_connect $button clicked => sub {
    my @sel = $datalist->get_selected_indices;
    my @row =  $datalist->{data}[$sel[0]];
    $self->set( active => scalar($row[0][0]) );
    hide $w;
  };
  $hbbox->pack_start($button,0,0,0);
  can_default $button 1;
  grab_default $button;
  show $button;
  show $w;
}

package Gimp::UI::PatternSelect;

our @ISA = 'Glib::Object';

Glib::Type->register (
   'Gimp::UI::PreviewSelect', __PACKAGE__,
   signals => {},
   properties => [],
);

sub get_title { Gimp::__"Pattern Selection Dialog" }
sub get_list { Gimp::Patterns->get_list("") }

sub new_pixbuf {
   my ($w,$h,$bpp,$mask)=Gimp::Pattern->get_pixels ($_);
   my $has_alpha = ($bpp==2 || $bpp==4);
   if ($bpp==1) {
      my @graydat = unpack "C*", $mask;
      my @rgbdat;
      foreach (@graydat) {
	 push @rgbdat, $_; push @rgbdat, $_; push @rgbdat, $_;
      }
      $mask = pack "C*", @rgbdat;
   } elsif($bpp == 3) {
      $mask = pack "C*", @{$mask};
   } elsif($bpp == 4) {
      $mask = pack "C*", @{$mask}[0..2];
   }
   # TODO: Add code/test for handling GRAYA; don't have any GRAYA to test
   # with currently though.
   Gtk2::Gdk::Pixbuf->new_from_data(
      $mask,'rgb', $has_alpha?1:0, 8, $w, $h, $has_alpha?$w*4:$w*3
   );
}

package Gimp::UI::BrushSelect;

our @ISA = 'Glib::Object';

Glib::Type->register (
   'Gimp::UI::PreviewSelect', __PACKAGE__,
   signals => {},
   properties => [],
);

sub get_title { Gimp::__"Brush Selection Dialog" }
sub get_list { Gimp::Brushes->get_list("") }

sub new_pixbuf {
   my ($w,$h,$mask_bpp,$mask,$color_bpp,$color_data) = Gimp::Brush->get_pixels($_);
   my @rgbdat;
   # color bitmaps seem broken from gimp's side, but I'm leaving it in here.
   # if you notice this and care, let me know and I may fix the gimp side.
   if ($color_bpp == 3) {
      @rgbdat = @{$color_data} ;
   } elsif ($color_bpp == 0) {
     my @graydat = @{$mask};
     foreach (@graydat) {
        $_ = 255 - $_;
        push @rgbdat, $_; push @rgbdat, $_; push @rgbdat, $_;
     }
   }
   Gtk2::Gdk::Pixbuf->new_from_data(pack("C*", @rgbdat),'rgb',0,8,$w,$h,$w*3);
}

package Gimp::UI::GradientSelect;

our @ISA = 'Glib::Object';

Glib::Type->register (
   'Gimp::UI::PreviewSelect', __PACKAGE__,
   signals => {},
   properties => [],
);

sub get_title { Gimp::__"Gradient Selection Dialog" }
sub get_list { Gimp::Gradients->get_list("") }

sub new_pixbuf {
   use POSIX;
   my @grad_row = map { $_ = abs(ceil($_*255 - 0.5)) }
                   Gimp::Gradient->get_uniform_samples ($_,100,0);
# make it 16 tall; there's bound to be a better way to do this? (it's slow)
   push @grad_row, @grad_row, @grad_row, @grad_row,
        @grad_row, @grad_row, @grad_row, @grad_row,
        @grad_row, @grad_row, @grad_row, @grad_row,
        @grad_row, @grad_row, @grad_row, @grad_row;
   my $pb = Gtk2::Gdk::Pixbuf->new_from_data(
      pack "C*", @grad_row,'rgb',1,8,100,8,100*4
   );
}

package Gimp::UI;

sub _new_adjustment {
   my @adj = eval { @{$_[1]} };
   $adj[2] ||= ($adj[1] - $adj[0]) * 0.01;
   $adj[3] ||= ($adj[1] - $adj[0]) * 0.01;
   $adj[4] ||= 0;
   new Gtk2::Adjustment $_[0], @adj;
}

# find a suitable value for the "digits" value
sub _find_digits {
   my $adj = shift;
   my $digits = log ($adj->step_increment || 1) / log(0.1);
   $digits > 0 ? int $digits + 0.9 : 0;
}

sub help_window(\$$$$) {
   my ($helpwin, $parent, $title, $blurb) = @_;
   unless ($$helpwin) {
      $$helpwin = new Gtk2::Dialog sprintf(__"Help for %s", $title), $parent, [];
      $$helpwin->action_area->set_border_width (2);
      my $tophelp = new Gtk2::Label $blurb;
      $tophelp->set_alignment(0.5,0.5);
      $tophelp->set_line_wrap(1);
      $$helpwin->vbox->pack_start($tophelp,0,1,3);
      my $sw = new Gtk2::ScrolledWindow undef,undef;
      $sw->set_policy (-automatic, -automatic);
      $sw->set_size_request(500,600);
      require Gtk2::Ex::PodViewer;
      my $pv = new Gtk2::Ex::PodViewer;
      require FindBin;
      $pv->load("$FindBin::RealBin/$FindBin::RealScript");
      $pv->show;
      $sw->add($pv);
      $$helpwin->vbox->add($sw);
      my $button = Gtk2::Button->new_from_stock('gtk-ok');
      signal_connect $button clicked => sub { hide $$helpwin };
      $$helpwin->action_area->add ($button);
      $$helpwin->signal_connect (destroy => sub { undef $$helpwin });
   }
   $$helpwin->show_all;
   $$helpwin->run;
   $$helpwin->hide;
}

sub _instrument {
  return unless $Gimp::verbose >= 2;
  my $obj = shift;
  my $class = ref $obj;
  my %sig2done;
  map {
    my $c = $_;
    map {
#warn "$c:$_->{signal_name}\n";
      my $s = $_->{signal_name};
      $obj->signal_connect(
	$s => sub { warn "SIG:$s(@_)\n";0 }
      ) unless $sig2done{$s};
      $sig2done{$s} = 1;
    } Glib::Type->list_signals($c);
  } Glib::Type->list_ancestors($class);
}

sub drawable_box {
  my $class = shift;
  my $a = Gtk2::HBox->new(0,5);
  my $b = $class->new;
  $a->pack_start($b, 1, 1, 0);
  my $c = Gtk2::Button->new("Refresh");
  $c->signal_connect("clicked", sub {$b->reload});
  $a->pack_start($c, 1, 1, 0);
  ($a, sub { }, sub { $b->get_active_scalar });
}

# function($name,$desc,$default,$extra,$value) returns $widget,\&setval,\&getval
my %PF2INFO = (
  &PF_STRING => sub {
    my $e = new Gtk2::Entry;
    ($e, sub { set_text $e $_[0] // "" }, sub { get_text $e });
  },
  &PF_SLIDER => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $adj = _new_adjustment ($value, $extra);
    my $a = new Gtk2::HScale $adj;
    $a->set_digits (_find_digits $adj);
    $a->set_size_request(120,-1);
    ($a, sub { $adj->set_value($_[0]) }, sub { $adj->get_value });
  },
  &PF_SPINNER => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $adj = _new_adjustment ($value, $extra);
    my $a = new Gtk2::SpinButton $adj, 1, 0;
    $a->set_digits (_find_digits $adj);
    ($a, sub { $adj->set_value($_[0]) }, sub { $adj->get_value });
  },
);
%PF2INFO = (
  %PF2INFO,
  &PF_FLOAT => $PF2INFO{&PF_STRING},
  &PF_FILE => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my ($a, $set, $get) = $PF2INFO{&PF_STRING}->();
    my $s = $a;
    $a = new Gtk2::HBox 0,5;
    $a->add ($s);
    my $b = new Gtk2::Button __"Browse";
    $a->add ($b);
    $b->signal_connect(clicked => sub {
      my $f = new Gtk2::FileChooserDialog
	sprintf(__"Choose %s", $name),
	undef, 'save', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok';
      $f->set_filename('.');
      $f->set_current_name($s->get_text);
      $f->show_all;
      $s->set_text($f->get_filename) if $f->run eq 'ok';
      $f->destroy;
      1;
    });
    ($a, $set, $get);
  },
  &PF_ADJUSTMENT => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my (@x)=@$default;
    $value=shift @x;
    $PF2INFO{pop(@x) ? &PF_SPINNER : &PF_SLIDER}->(
      $name, $desc, $default, [@x], $value,
    );
  },
  &PF_INT8 => sub { $PF2INFO{&PF_SLIDER}->(@_[0..2], [ 0, 255, 1 ], $_[4]); },
  &PF_INT16 => sub {
    my $max = 1 << 15;
    $PF2INFO{&PF_SPINNER}->(@_[0..2], [ -$max, $max - 1, 1 ], $_[4]);
  },
  &PF_INT32 => sub {
    my $max = 1 << 31;
    $PF2INFO{&PF_SPINNER}->(@_[0..2], [ -$max, $max - 1, 1 ], $_[4]);
  },
  &PF_FONT => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    $default = 'Sans' unless $default;
    $value = $default unless $value;
    my ($a, $s, $g);
    if ($Gimp::interface_pkg ne 'Gimp::Net') {
      $a = new Gimp::UI::FontSelectButton $desc, $default;
      $s = sub { $a->set_font($_[0] || 'Arial') };
      $g = sub { $a->get_font };
#      _instrument($a);
    } else {
      # no GIMP ui available, use Gtk2 equivalent
      my $fs = new Gtk2::FontSelectionDialog sprintf __"Font Selection Dialog (%s)", $desc;
      my $val;
      my $l = new Gtk2::Label "!error!";
      my $setval = sub {
	$val = shift // '';
	$val =~ s#\s*(Bold)?\s*(Italic)?\s*\d+$##; # vim highlighter
	unless (defined $val && $fs->set_font_name ("$val 10")) {
	  warn sprintf __"Illegal default font description: %s\n", $val
	    if defined $val;
	  $val = $default;
	  $fs->set_font_name ("$val 10");
	}
	$l->set (label => " $val ");
      };
      $fs->ok_button->signal_connect (clicked => sub {$setval->($fs->get_font_name); $fs->hide});
      $fs->cancel_button->signal_connect (clicked => sub {$fs->hide});
      $s = $setval;
      $g = sub { $val };
      $a = new Gtk2::Button;
      $a->add ($l);
      $a->signal_connect (clicked => sub { show $fs });
    }
    ($a, $s, $g);
  },
  &PF_COLOR => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my ($a, $s, $g);
    if ($Gimp::interface_pkg eq 'Gimp::Net') {
      $a = Gtk2::ColorButton->new;
      $s = sub {
	my $colour = Gimp::canonicalize_color(shift // [0.8,0.6,0.1]);
	my @rgb = map { $_ * ((1<<16)-1) } @$colour[0..2];
	$a->set_color(Gtk2::Gdk::Color->new(@rgb));
      };
      $g = sub {
	my $gc = $a->get_color;
	my @c = map { $_ / ((1<<16)-1) } $gc->red, $gc->green, $gc->blue;
	\@c;
      };
    } else {
      $default = Gimp::canonicalize_color($default // [0.8,0.6,0.1]);
      $a = new Gimp::UI::ColorButton $desc, 90, 14, $default, 'small-checks';
#      _instrument($a);
      $s = sub {
	$a->set_color(Gimp::canonicalize_color(shift // [0.8,0.6,0.1]));
      };
      $g = sub { $a->get_color };
    }
    ($a, $s, $g);
  },
  &PF_TOGGLE => sub {
    my $a = new Gtk2::CheckButton;
    ($a, sub{ $a->set (active => $_[0] ? 1 : 0)}, sub{ $a->get("active") });
  },
  &PF_RADIO => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $b = new Gtk2::HBox 0,5;
    my ($r,$prev);
    my $prev_sub = sub { $r = $_[0] };
    while (@$extra) {
      my $label = shift @$extra;
      my $value = shift @$extra;
      my $radio = new Gtk2::RadioButton undef, $label;
      $radio->set_group ($prev) if $prev;
      $b->pack_start ($radio, 1, 0, 5);
      $radio->signal_connect (clicked => sub { $r = $value });
      my $prev_sub_my = $prev_sub;
      $prev_sub = sub { $radio->set_active ($_[0] eq $value); &$prev_sub_my };
      $prev = $radio;
    }
    $a = new Gtk2::Frame;
    $a->add($b);
    ($a, $prev_sub, sub { $r });
  },
  &PF_IMAGE => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $a = Gtk2::HBox->new(0,5);
    my $b = Gimp::UI::Combo::Image->new;
    $a->pack_start ($b,1,1,0);
    my $load = Gtk2::Button->new('Browse');
    $a->pack_start ($load,1,1,0);
    $load->signal_connect (clicked => sub {
      my $f = new Gtk2::FileChooserDialog
	sprintf(__"Load %s", $name),
	undef, 'open', 'gtk-cancel' => 'cancel', 'gtk-open' => 'ok';
      $f->set_filename ('.');
      $f->show_all;
      my $result = $f->run;
      if ($result eq 'ok') {
	my $i = Gimp->file_load($f->get_filename, $f->get_filename);
	eval { Gimp::Display->new($i); };
	$b->reload;
      }
      $f->destroy;
      1;
    });
    my $c = Gtk2::Button->new("Refresh");
    $c->signal_connect("clicked", sub {$b->reload});
    $a->pack_start ($c,1,1,0);
    ($a, sub { }, sub { $b->get_active_scalar });
  },
  &PF_LAYER => sub { drawable_box('Gimp::UI::Combo::Layer'); },
  &PF_CHANNEL => sub { drawable_box('Gimp::UI::Combo::Channel'); },
  &PF_DRAWABLE => sub { drawable_box('Gimp::UI::Combo::Drawable'); },
  &PF_PATTERN => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $a=new Gimp::UI::PatternSelect;
    ($a,
      sub { $a->set('active', $value // (Gimp::Context->get_pattern)[0]) },
      sub { $a->get('active') });
  },
  &PF_BRUSH => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $a=new Gimp::UI::BrushSelect;
    ($a,
      sub{ $a->set('active', $value // (Gimp::Context->get_brush)[0]) },
      sub{ $a->get('active') });
  },
  &PF_GRADIENT => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    $a=new Gimp::UI::GradientSelect;
    ($a,
      sub { $a->set('active', $value // (Gimp::Gradients->get_list(""))[0]) },
      sub { $a->get('active') });
  },
  &PF_CUSTOM => sub { goto &{$_[3]}; },
  &PF_TEXT => sub {
    my ($name,$desc,$default,$extra,$value) = @_;
    my $a = new Gtk2::Frame;
    my $h = new Gtk2::VBox 0,5;
    $a->add($h);
    my $b = new Gtk2::TextBuffer;
    my $e = new_with_buffer Gtk2::TextView $b;
    $e->set_size_request(300,200);
    $e->set_wrap_mode('GTK_WRAP_WORD');
    $e->set_editable (1);
    $h->add ($e);
    my $buttons = new Gtk2::HBox 1,5;
    $h->add ($buttons);
    my $sv = sub { $b->set_text ($_[0]); };
    my $gv = sub {$b->get_text ($b->get_start_iter, $b->get_end_iter, 0);};
    my $load = Gtk2::Button->new_from_stock('gtk-open');
    my $save = Gtk2::Button->new_from_stock('gtk-save');
    my $edit = Gtk2::Button->new_from_stock('gimp-edit');
    $buttons->add ($load);
    $buttons->add ($save);
    $buttons->add ($edit);
    $edit->signal_connect (clicked => sub {
      my $tmp = Gimp->temp_name("txt");
      io($tmp)->utf8->print(&$gv);
      system 'gedit', $tmp;
      $sv->(io($tmp)->utf8->all);
    });
    $load->signal_connect (clicked => sub {
      my $f = new Gtk2::FileChooserDialog
	sprintf(__"Load %s", $name),
	undef, 'open', 'gtk-cancel' => 'cancel', 'gtk-open' => 'ok';
      $f->set_filename ('.');
      $f->show_all;
      $sv->(io($f->get_filename)->utf8->all) if $f->run eq 'ok';
      $f->destroy;
      1;
    });
    $save->signal_connect (clicked => sub {
      my $f = new Gtk2::FileChooserDialog
	sprintf(__"Save %s", $name),
	undef, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'ok';
      $f->set_filename ('.');
      $f->show_all;
      io($f->get_filename)->utf8->print(&$gv) if $f->run eq 'ok';
      $f->destroy;
      1;
    });
    ($a, $sv, $gv);
  },
);

sub interact {
  warn __PACKAGE__ . "::interact(@_)" if $Gimp::verbose >= 2;
  my ($function, $blurb, $help, $params, $menupath) = splice @_, 0, 5;
  my (@getvals, @setvals, @lastvals, @defaults);
  my $helpwin;

  Gimp::gtk_init;
  my $exception_text;
  my $exception = sub { $exception_text = $_[0]; Gtk2->main_quit; };
  Glib->install_exception_handler($exception);
  Glib::Log->set_handler(
    'GLib-GObject', [
      qw(G_LOG_FATAL_MASK G_LOG_LEVEL_CRITICAL G_LOG_LEVEL_ERROR
      G_LOG_FLAG_FATAL G_LOG_LEVEL_WARNING)
    ], $exception
  );

  my $t = new Gtk2::Tooltips;
  my $title = $menupath;
  $title =~ s#.*/##; $title =~ s#[_\.]##g;
  my $w = new Gtk2::Dialog "Perl-Fu: $title", undef, [];
  $w->set_border_width(3); # sets border on inside because it's a window
  $w->action_area->set_spacing(2);
  $w->action_area->set_homogeneous(0);
  my $topblurb = new Gtk2::Label $blurb;
  $topblurb->set_alignment(0.5,0.5);
  $w->vbox->pack_start($topblurb,0,1,3);
  my $table = new Gtk2::Table scalar @$params,2,0;
  $table->set(border_width => 4);
  my $row = 0;
  for(@$params) {
    my ($type,$name,$desc,$default,$extra)=@$_;
    my ($value)=shift;
    $value=$default unless defined $value;
    die sprintf __"Unsupported argumenttype %s for %s\n", $type, $name
      unless $PF2INFO{$type};
    my ($a, $sv, $gv) = $PF2INFO{$type}->($name,$desc,$default,$extra,$value);
    push @setvals, $sv;
    push @getvals, $gv;
    push @lastvals, $value;
    push @defaults, $default;
    $sv->($value);
    my $label = new Gtk2::Label "$desc: ";
    $label->set_alignment(1.0,0.5);
    $table->attach($label, 0, 1, $row, $row+1, ["expand","fill"], ["expand","fill"], 4, 2);
    my $halign = new Gtk2::HBox 0,0;
    $halign->pack_start($a,0,0,0);
    $table->attach($halign, 1, 2, $row, $row+1, ["expand","fill"], ["expand","fill"], 4, 2);
    $row++;
  }
  my $sw = new Gtk2::ScrolledWindow undef,undef;
  $sw->set_policy (-automatic, -automatic);
  $sw->add_with_viewport($table);
  $w->vbox->add($sw);

  my $mainloop = Glib::MainLoop->new;
  my $button = $w->add_button('gtk-help', 3);
  $button->signal_connect(clicked => sub {
    help_window($helpwin, $w, $title, $blurb);
  });
  $button = $w->add_button('gimp-reset', 2);
  $button->signal_connect(clicked => sub {
    map { $setvals[$_]->($defaults[$_]); } (0..$#defaults)
  });
  set_tip $t $button,__"Reset all values to their default";
  my $res = 0;
  $button = $w->add_button('gtk-cancel', 0);
  $button->signal_connect(clicked => sub {
    $mainloop->quit;
  });
  can_default $button 1;
  $button = $w->add_button('gtk-ok', 1);
  $button->signal_connect(clicked => sub {
    $res = 1;
    $mainloop->quit;
  });
  can_default $button 1;
  grab_default $button;
  $w->signal_connect(destroy => sub { $mainloop->quit; });
  set_transient($w);

  show_all $table;
  show_all $sw;
  $sw->set_size_request(
    min(0.75*$sw->get_screen->get_width, $table->size_request->width + 30),
    min(0.6*$sw->get_screen->get_height, $table->size_request->height + 5)
  );
  show_all $w;
  $mainloop->run;
  die $exception_text if $exception_text;
  my @retvals = map {&$_} @getvals if $res;
  $w->destroy;
  return unless $res;
  return (1, @retvals);
}

1;
__END__

=head1 NAME

Gimp::UI - Programming interface to libgimpui, plus Gtk widgets for other
parameter types.

=head1 SYNOPSIS

  use Gimp::UI;

=head1 DESCRIPTION

If you use L<Gimp::Fu> in your script, a GUI will be taken care of
for you. However, for an example of implementing your own UI, see
C<examples/example-no-fu>.

=over 4

 $combo_box = new Gimp::UI::Combo::Image;
 $combo_box = new Gimp::UI::Combo::Layer;

 $button = new Gimp::UI::PatternSelect;
 $button = new Gimp::UI::BrushSelect;
 $button = new Gimp::UI::GradientSelect;

 ($result, @new_vals) = Gimp::UI::interact(
   $functionname, $blurb, $help, $params, $menupath, @previous_vals
 ); # $result = true if "Ok", false if "Cancel"

=back

=head1 AUTHOR

Marc Lehmann <pcg@goof.com>, Seth Burgess <sjburges@gimp.org>

=head1 SEE ALSO

perl(1), L<Gimp>.