The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/perl

#BEGIN {$^W=1};

use Gimp::Feature qw(perl-5.005 gtk-1.2);
use Gimp (':consts','__','N_');
use Gimp::Fu;
use Gtk;
use Gtk::Gdk;
use Gimp::UI (); # for the logo
use POSIX 'strftime';

#Gimp::set_trace(TRACE_ALL);

my $ex;		# average font width for default font
my $ey;		# average font height for default font

my $window;	# the main window
my $clist;	# the list of completions
my $rlist;	# the results list
my $inputline;	# the input entry
my $result;	# the result entry
my $synopsis;	# the synopsis label
my $statusbar;	# the statusbar
my $cinfo;	# command info

my $idle;	# the idle function id

my($blurb,$help,$author,$copyright,$date,$type,$args,$results);
my @args;	# the arguments of the current function

my @function;	# the names of all functions
my %function;	# the same as hash
my @completion;	# list of completions
my @compdata;	# index completion -> data
my %plugin_info;# function -> [...]

sub refresh {
   undef %function;
   @function = Gimp->procedural_db_query("","","","","","","");
   @function{@function}=(1) x @function;
   eval {
      my ($a, $b, $c, $d, $e, $f) = Gimp->plugins_query("");
      for $i (0..$#$a) {
         $plugin_info{$f->[$i]} = [map $_->[$i], $a, $b, $c, $d, $e];
      }
   }
}

sub get_words {
   my $text = $inputline->get_text;
   my $i = 0;
   my($p,$idx,$pos);
   my $word;
   my @words;
   substr($text,$inputline->get('text_position'),0,"\0");
   while ($text =~ /("(?:[^"\\]*(?:\\.[^"\\]*)*)")[ ,]*|([^ ,]+)[ ,]*|[ ,]+/g) {
      $word = defined $1 ? $1 : $2;
      if (($p = index($word, "\0")) >= 0) {
         $idx=$i; $pos=$p;
         substr ($word, $p, 1, "");
      }
      $i++;
      push(@words,$word);
   }
   ($idx,$pos,@words);
}

sub set_words {
   my $text=shift;
   $text.=" ".join(",",@_) if scalar@_;
   my $pos=index($text,"\0");
   if ($pos) {
      substr($text,$pos,1,"");
      $inputline->set_text($text);
      $inputline->set_position($pos);
   } else {
      $inputline->set_text($text);
   }
}

my $last_func;
my $last_arg;

my %type2str = (
   &PDB_BOUNDARY    => 'BOUNDARY',
   &PDB_CHANNEL     => 'CHANNEL',
   &PDB_COLOR       => 'COLOR',
   &PDB_DISPLAY     => 'DISPLAY',
   &PDB_DRAWABLE    => 'DRAWABLE',
   &PDB_FLOAT       => 'FLOAT',
   &PDB_IMAGE       => 'IMAGE',
   &PDB_INT32       => 'INT32',
   &PDB_FLOATARRAY  => 'FLOATARRAY',
   &PDB_INT16       => 'INT16',
   &PDB_PARASITE    => 'PARASITE',
   &PDB_STRING      => 'STRING',
   &PDB_PATH        => 'PATH',
   &PDB_INT16ARRAY  => 'INT16ARRAY',
   &PDB_INT8        => 'INT8',
   &PDB_INT8ARRAY   => 'INT8ARRAY',
   &PDB_LAYER       => 'LAYER',
   &PDB_REGION      => 'REGION',
   &PDB_STRINGARRAY => 'STRINGARRAY',
   &PDB_SELECTION   => 'SELECTION',
   &PDB_STATUS      => 'STATUS',
   &PDB_INT32ARRAY  => 'INT32ARRAY',
);

sub setheight {
   my($w,$y)=@_;
   $w->set_usize(-1, ($w->style->font->ascent + $w->style->font->descent) * $y);

}

sub new_cinfo {
   $cinfo->freeze;
   $cinfo->clear;

   my $add_split = sub {
      my($t,$n,$d)=@_;
      $d=~s/^(.{40,60})[ \t]*([\{[:\(])/$1\n$2/mg;
      for(split/\n/,Gimp::wrap_text($d,60)) {
         $cinfo->append("",$t,$n,$_);
         $t=$n="";
      }
   };
   
   if($args) {
      $cinfo->append("In:","","","");
      for(@args) {
         $add_split->($type2str{$_->[0]},$_->[1],$_->[2]);
      }
   }
   if($results) {
      $cinfo->append("Out:","","","");
      for(0..$results-1) {
         my($type,$name,$desc)=Gimp->procedural_db_proc_val ($last_func, $_);
         $add_split->($type2str{$type},$name,$desc);
      }
   }

   $cinfo->thaw;
}

sub set_current_function {
   my $fun = shift;
   return if $last_func eq $fun || !$function{$fun};
   $last_func = $fun;
   $last_arg = 0;
   @args=();
   eval {
      $function{$fun} or die;
      ($blurb,$help,$author,$copyright,$date,$type,$args,$results)=
         Gimp->procedural_db_proc_info($fun);
      $blurb_label->set($blurb);
      for(0..$args-1) {
         push(@args,[Gimp->procedural_db_proc_arg($fun,$_)]);
      }
      new_cinfo;

      $help_text->delete_text(0,-1);
      $help_text->insert_text($help,0);
      $author_label->set($author);
      $copyright_label->set($copyright);
      $date_label->set($date);
      eval {
         my ($menupath, $accel, $path, $imagetypes, $mtime) = @{$plugin_info{$fun}};
         $menupath_label->set($menupath);
         $accelerator_label->set($accel);
         $plugin_path_label->set($path);
         $imagetypes_label->set($imagetypes);
         $last_modified_label->set(strftime("%Y-%m-%d %H:%M:%S (%Z)", localtime ($mtime)));
      };
      warn $@ if $@;
   };
   warn $@ if $@;
}

sub set_clist {
   $clist->freeze;
   $clist->clear;
   @completion = ();
   @compdata = ();
   while(@_) {
      $clist->append(@_[0]);
      push @completion, shift;
      push @compdata, shift;
   }
   $clist->thaw;
}

sub complete_function {
   my $name = shift;
   $name=~s/[-_]/[-_]/g;
   my @matches = eval { sort grep /$name/i,@function };
   if(@matches>1) {
      set_clist map(($_,$_),@matches);
      $synopsis->set(scalar@matches.__" matching functions");
   } else {
      set_clist @matches,@matches;
      $synopsis->set($matches[0].__" (press Tab to complete)");
   }
}

sub complete_type {
   my($type,$name,$desc)=@_;

   if($type==PDB_IMAGE) {
      set_clist(map(("$$_: ".$_->get_filename,$$_),Gimp->image_list));
   } elsif($type==PDB_LAYER) {
      set_clist(map { my $i = $_; map(("$$_: ".$i->get_filename."/".$_->get_name,$$_),$i->get_layers)} Gimp->image_list);
   } elsif($type==PDB_CHANNEL) {
      set_clist(map { my $i = $_; map(("$$_: ".$i->get_filename."/".$_->get_name,$$_),$i->get_channels)} Gimp->image_list);
   } elsif($type==PDB_DRAWABLE) {
      set_clist(map { my $i = $_; map(("$$_: ".$i->get_filename."/".$_->get_name,$$_),($i->get_layers,$i->get_channels))} Gimp->image_list);
   } elsif ($type==PDB_INT32) {
      if ($name eq "run_mode") {
         set_clist("RUN_NONINTERACTIVE","RUN_NONINTERACTIVE",
                   "RUN_INTERACTIVE","RUN_INTERACTIVE",
                   "RUN_WITH_LAST_VALS","RUN_WITH_LAST_VALS");
      } elsif ($desc=~s/(?::\s*)?{(.*)}.*?$//) {
         $_=$1;
         my @args;
         while(s/^.*?([A-Za-z_-]+)\s*\(\s*(\d+)\s*\)//) {
            push(@args,"$2: $1",$2);
         }
         set_clist(@args);
      } else {
         set_clist;
      }
   } else {
      set_clist;
   }
   $synopsis->set($desc);
}

sub update_completion {
   my($idx,$pos,@words)=get_words;

   return unless $idx ne $last_arg;
   $last_arg=$idx;

   set_current_function $words[0];

   if ($idx == 0) {
      complete_function($words[0]);
   } elsif ($idx>@args) {
      $synopsis->set(__"too many arguments");
      set_clist;
   } else {
      complete_type(@{$args[$idx-1]});
   }
}

sub do_completion {
   update_completion;

   my($idx,$pos,@words)=get_words;
   my($word)=$words[$idx];

   $word=~s/[-_]/[-_]/g;
   my(@matches)=grep /$word/i,@completion;
   my $new;
   if (@matches>1) {
      if (join("\n",@matches) =~ ("^(".$words[$idx].".*).*?".("\n\\1.*" x scalar@matches-1))) {
         $new=$1;
      }
   } elsif(@matches==1) {
      $new=$compdata[0];
   } else {
      Gtk::Gdk->beep;
   }
   if (defined $new) {
      $words[$idx]=$new;
      set_current_function $words[0] if $idx==0;
      if($idx<@args) {
         $words[$idx+1]="\0".$words[$idx+1];
      } else {
         $words[$idx].="\0";
      }
      set_words @words;
   }
   undef $last_arg;
}

sub execute_command {
   my($idx,$pos,$fun,@args)=get_words;
   $res=eval { Gimp->$fun(@args) };
   if ($@) {
      $statusbar->set($@);
      $result->set_text("");
      Gtk::Gdk->beep;
   } else {
      $statusbar->set('');
      $result->set_text($res);
      $rlist->prepend_items(new Gtk::ListItem $res);
   }
}

sub idle {
   Gtk->idle_remove($idle) if $idle;
   undef $idle;
   update_completion;
}

sub do_idle {
   $idle=Gtk->idle_add(\&idle) unless $idle;
}

eval "use Gtk::Keysyms ()";
$Gtk::Keysyms{Tab} ||= 0xFF09;

sub inputline {
   my $e = new Gtk::Entry;
   $e->set_text("");
   $e->signal_connect("changed",sub {
      undef $last_arg;
      do_idle;
   });
   $e->signal_connect("focus_in_event",\&do_idle);
   $e->signal_connect("button_press_event",\&do_idle);
   $e->signal_connect("key_press_event",sub {
      undef $last_arg;
      do_idle;
      if ($_[1]->{keyval} == $Gtk::Keysyms{Tab}) {
          $_[0]->signal_emit_stop_by_name('key_press_event');
         do_completion;
         1;
      } else {
         ();
      }
   });
   $e->signal_connect("activate",\&execute_command);
   #$e->set_usize($ex*40,0);
   $inputline=$e;

   my $c = new Gtk::CList(1);
   setheight $c, 6;
   $clist = $c;
   $c->set_selection_mode(-extended);
   $c->signal_connect("select_row", sub {
      eval {
         my($idx,$pos,@words)=get_words;
         $words[$idx]=$compdata[$_[1]]."\0";
         set_words (@words);
         set_current_function (substr($words[0],0,-1)) unless $idx;
      };
      do_idle;
   });

   my $r = new Gtk::List;
   $rlist = $r;
   $r->set_selection_mode(-single);
   $r->set_selection_mode(-browse);
}

sub info {
   my $info = new Gtk::Dialog;
   $info->set_title(__"Function Info");
   $info->signal_connect(delete_event => sub { $info->hide });

   my $close = new Gtk::Button __"Close";
   $close->signal_connect(clicked => sub { $info->hide });
   $info->action_area->add($close);

   my $table = new Gtk::Table 2,9+1,0;
   my $y = 0;
   $info->vbox->add($table);

   local *add_info = sub {
      my ($label, $widget, $large) = @_;
      $label = new Gtk::Label $label.": ";
      $label->set_alignment(0, 0);
      $table->attach($label,0,1,$y,$y+1,[-fill],[-fill],0,0);
      if ($large) {
         $y++;
         $table->attach($widget,0,2,$y,$y+$large,[-expand,-fill],[-expand,-fill],0,0);
         $y+=$large;
      } else {
         $widget->set_alignment(0, 0);
         $table->attach($widget,1,2,$y,$y+1,[-fill],[-fill],0,0);
         $y++;
      }
   };

   $blurb_label = new Gtk::Label;
   add_info(__"Menu Path", $menupath_label = new Gtk::Label);
   add_info(__"Accelerator", $accelerator_label = new Gtk::Label);
   add_info(__"Image Types", $imagetypes_label = new Gtk::Label);
   add_info(__"Author", $author_label = new Gtk::Label);
   add_info(__"Copyright", $copyright_label = new Gtk::Label);
   add_info(__"Date/Version", $date_label = new Gtk::Label);
   add_info(__"Last Modified", $last_modified_label = new Gtk::Label);
   add_info(__"Plug-In Path", $plugin_path_label = new Gtk::Label);

   $help_text = new Gtk::Text;
   $help_text->set_editable(0);
   $help_text->set_word_wrap(1);
   my $cs = new Gtk::ScrolledWindow undef,undef;
   $cs->set_policy(-automatic,-automatic);
   $cs->add ($help_text);
   add_info (__"Description", $cs, 2);

   my $h = new Gtk::HBox(0,5);
   my $more = new Gtk::Button __"More...";
   $more->signal_connect(clicked => sub { $info->visible ? $info->hide : $info->show_all });
   $h->add($blurb_label);
   $h->add($more);
   $h;
}

sub create_main {
   my $b;
   my $t;

   $t = new Gtk::Tooltips;
   my $w = new Gtk::Dialog;
   $window = $w;
   $w->realize;
   $ex = $w->style->font->string_width ('Mn')*0.5;
   $ey = $w->style->font->string_width ('My');

   $w->set_title(__"PDB Explorer - the olof edition (yet still an alpha version)");
   $w->signal_connect("destroy",sub {main_quit Gtk});

   $b = new Gtk::Button __"Close";
   $w->action_area->add($b);
   $b->signal_connect("clicked",sub {main_quit Gtk});

   my $vpane = new Gtk::VPaned; $w->vbox->add($vpane);
   $vpane->add1(my $f1 = new Gtk::VBox 0,0);
   $vpane->add2(my $f2 = new Gtk::VBox 0,0);

   my $h = new Gtk::HBox (0,5);
   $f1->pack_start ($h,0,0,0);

   inputline;

   $synopsis = new Gtk::Label "";
   $synopsis->set_justify(-left);

   my $table = new Gtk::Table 3,4,0;
   $f1->pack_start($table,1,1,0);

   my $cs = new Gtk::ScrolledWindow undef,undef;
   $cs->set_policy(-automatic,-automatic);
   $cs->add ($clist);
   #$cs->set_usize(0,$ey*6);

   my $rs = new Gtk::ScrolledWindow undef,undef;
   $rs->set_policy(-automatic,-automatic);
   $rs->add_with_viewport ($rlist);

   $result = new Gtk::Entry;
   $result->set_editable(0);
   #$result->set_usize($ex*30,0);

   $statusbar = new Gtk::Label;

   $table->border_width(10);

   $table->attach(new Gtk::Label(__"Synopsis") ,0,1,0,1,{},{},0,0);
      $table->attach($synopsis ,1,2,0,1,{},{},0,0);
         $table->attach(Gimp::UI::logo($w),2,3,0,1,{},{},0,0);
   $table->attach(new Gtk::Label(__"Command")  ,0,1,1,2,{},{},0,0);
      $table->attach($inputline,1,3,1,2,['expand','fill'],{},0,0);
#         $table->attach($result,2,3,1,2,['expand','fill'],{},0,0);
   $table->attach(new Gtk::Label(__"Shortcuts"),0,1,2,3,{},{},0,0);
      $table->attach($cs       ,1,3,2,3,['expand','fill'],['expand','fill'],0,0);
#         $table->attach($rs,2,3,2,3,['expand','fill'],['expand','fill'],0,0);
   $table->attach(new Gtk::Label(__"Status"),0,1,3,4,{},{},0,0);
   $table->attach($statusbar,1,3,3,4,{},{},0,0);

   $f2->pack_start(info,0,1,5);
   my $sw = new Gtk::ScrolledWindow;
   $sw->set_policy(-automatic, -automatic);
   $cinfo = new_with_titles Gtk::CList '',__"TYPE",__"NAME",__"DESCRIPTION";
   $cinfo->set_column_auto_resize (0,1);
   $cinfo->set_column_auto_resize (1,1);
   $cinfo->set_column_auto_resize (2,1);
   $cinfo->set_selection_mode('single');
   setheight $cinfo, 8;
   $sw->add ($cinfo);
   $f2->pack_start ($sw,1,1,5);

   idle;

   $w->realize;
   show_all $w;
}

register "extension_pdb_explorer",
         "Procedural Database Explorer",
         "This is a more interactive and less broken / script-fu-centric version of the DB Browser",
         "Marc Lehmann",
         "Marc Lehmann",
         "0.4alpha",
         N_"<Toolbox>/Xtns/PDB Explorer...",
         "",
         [],
         sub {

   Gimp::gtk_init;
   refresh;
   create_main;
   main Gtk;

   ();
};

exit main;