File Coverage

blib/lib/Tk/DBIx/Tree.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Tk::DBIx::Tree;
2             #------------------------------------------------
3             # automagically updated versioning variables -- CVS modifies these!
4             #------------------------------------------------
5             our $Revision = '$Revision: 1.2 $';
6             our $CheckinDate = '$Date: 2003/11/06 17:55:56 $';
7             our $CheckinUser = '$Author: xpix $';
8             our $Version = 1.5;
9             # we need to clean these up right here
10             $Revision =~ s/^\$\S+:\s*(.*?)\s*\$$/$1/sx;
11             $CheckinDate =~ s/^\$\S+:\s*(.*?)\s*\$$/$1/sx;
12             $CheckinUser =~ s/^\$\S+:\s*(.*?)\s*\$$/$1/sx;
13             #-------------------------------------------------
14             #-- package Tk::DBIx::Tree -----------------------
15             #-------------------------------------------------
16            
17            
18 1     1   4450 use DBIx::Tree;
  1         45578  
  1         55  
19 1     1   1694 use Tk::Tree;
  0            
  0            
20             use Tk::Compound;
21             use Tk::ItemStyle;
22             use Tk::ResizeButton;
23             use Term::ANSIColor;
24             use base qw/Tk::Derived Tk::Frame/;
25            
26             use strict;
27            
28             Construct Tk::Widget 'DBITree';
29            
30             # ------------------------------------------
31             sub ClassInit
32             # ------------------------------------------
33             {
34             my($class,$mw) = @_;
35            
36             }
37            
38             # ------------------------------------------
39             sub Populate {
40             # ------------------------------------------
41             my ($obj, $args) = @_;
42             my $style;
43            
44             $obj->{dbh} = delete $args->{'-dbh'} || return error("No DB-Handle!");
45             $obj->{table} = delete $args->{'-table'} || return error("No Table!");
46             $obj->{debug} = delete $args->{'-debug'} || 0;
47             $obj->{idx} = delete $args->{'-idx'} || return error("No IndexColumn!");
48             $obj->{fields} = delete $args->{'-fields'} || return error("No Fields!");
49             $obj->{textcolumn} = delete $args->{'-textcolumn'} || return error("No Textcolumn!");
50             $obj->{joiner} = delete $args->{'-joiner'};
51             $obj->{start_id} = delete $args->{'-start_id'} || 1;
52             $obj->{command} = delete $args->{'-command'};
53             $obj->{parent_id} = delete $args->{'-parent_id'} || return error("No Parent_id!");
54             $obj->{columnWidths} = delete $args->{'-columnWidths'};
55             $obj->{maxchars} = delete $args->{'-maxchars'};
56             $obj->{colNames} = delete $args->{'-colNames'};
57             $obj->{entry_create_cb} = delete $args->{'-entry_create_cb'};
58             $obj->{time_column} = delete $args->{'-time_column'};
59             $obj->{opencmd} = delete $args->{'-opencmd'};
60             $obj->{closecmd} = delete $args->{'-closecmd'};
61            
62             my $h_style = delete $args->{'-highlight'} || [-foreground => 'blue'];
63             my $n_style = delete $args->{'-normal'} || [-foreground => 'black'];
64             $obj->{highlight} = $obj->ItemStyle('imagetext', @{$h_style});
65             $obj->{normal} = $obj->ItemStyle('imagetext', @{$n_style});
66            
67             $obj->SUPER::Populate($args);
68            
69            
70             my %specs;
71             $specs{refresh} = [qw/METHOD refresh Refresh/, undef];
72             $specs{close_all} = [qw/METHOD close_all Close_all/, undef];
73             $specs{listEntrys} = [qw/METHOD listEntrys ListEntrys/, undef];
74             $specs{remember} = [qw/METHOD remember Remember/, undef];
75             $specs{select_entrys} = [qw/METHOD select_entrys Select_entrys/, undef];
76             $specs{info} = [qw/METHOD info Info/, undef];
77             $specs{infozoom} = [qw/METHOD infozoom InfoZoom/, undef];
78             $specs{color_all} = [qw/METHOD color_all Color_All/, undef];
79             $specs{color_clear} = [qw/METHOD color_clear Color_Clear/, undef];
80             $specs{get_id} = [qw/METHOD get_id Get_Id/, undef];
81             $specs{parent_id} = [qw/METHOD parent_id Parent_Id/, undef];
82             $specs{see} = [qw/METHOD see See/, undef];
83             $specs{childs} = [qw/METHOD childs Childs/, undef];
84            
85             $specs{neu} = [qw/METHOD neu Neu/, undef];
86             $specs{move} = [qw/METHOD move Move/, undef];
87             $specs{copy} = [qw/METHOD copy Copy/, undef];
88             $specs{dele} = [qw/METHOD dele Dele/, undef];
89             $specs{refresh_id} = [qw/METHOD refresh_id Refresh_Id/, undef];
90            
91             $obj->ConfigSpecs(%specs);
92            
93             $obj->{last_refresh_time} = 1;
94            
95             $obj->{tree} = $obj->Scrolled('Tree',
96             -scrollbars => 'osoe',
97             -columns => scalar @{$obj->{fields}} + 1,
98             -header => 1,
99             -separator => ':',
100             )->pack(-expand => 1,
101             -fill => 'both');
102            
103             $obj->{tree}->configure(-opencmd => sub{
104             $obj->{OPEN}->{$_[0]} = 'offen';
105             my $ok = 1;
106             $ok = &{$obj->{opencmd}}(@_)
107             if(defined $obj->{opencmd} and ref $obj->{opencmd} eq 'CODE');
108             if($ok) {
109             $obj->refresh( undef, $_[0] );
110             }
111             $obj->{tree}->OpenCmd(@_)
112             });
113             $obj->{tree}->configure(-closecmd => sub{
114             my $path = $_[0];
115             my $ok = 1;
116             foreach my $item (keys %{$obj->{OPEN}}) {
117             delete $obj->{OPEN}->{$item}
118             if($item =~ /^$path/);
119             }
120             $ok = &{$obj->{closecmd}}(@_)
121             if(defined $obj->{closecmd} and ref $obj->{closecmd} eq 'CODE');
122             if($ok) {
123             $obj->{tree}->CloseCmd(@_)
124             }
125             });
126            
127             $obj->Advertise("tree" => $obj->{tree});
128            
129             } # end Populate
130            
131            
132             # Class private methods;
133             # ------------------------------------------
134             sub refresh_id {
135             # ------------------------------------------
136             my $obj = shift || return error('No Object');
137             my $path = shift || return error('No Id');
138             my $data = shift || $obj->info('data', $path);
139            
140             $path = $obj->id2path($path);
141            
142             return unless($obj->info('exists',$path));
143             my ($id, $pid) = $obj->id($path);
144             $obj->dele($path);
145             $obj->neu($id, $pid, $data);
146             }
147            
148             # ------------------------------------------
149             sub neu {
150             # ------------------------------------------
151             my $obj = shift || return error('No Object');
152             my $id = shift || return error('No Id');
153             my $to_parent = shift || return error('No To Id');
154             my $data = shift || return error('No Data');
155             my $ignore_status = shift || 0;
156            
157             $obj->debug('Neu - Id: %s, To_Parent: %s, Data %s', $id, $to_parent, $data);
158            
159             $data->{$obj->{idx}} = $id
160             unless $data->{$obj->{idx}};
161            
162             my ($iid, $pid) = $obj->id($id);
163             my $new_path = sprintf('%s:%d', $to_parent, $iid );
164            
165             $obj->{tree}->setmode($to_parent, 'open')
166             if($obj->{tree}->getmode($to_parent) eq 'none');
167            
168             $obj->debug('Ignore Status? <%s> Parent Exists? <%s> Getmode(Parent) <%s>',
169             ($ignore_status ? 'yes' : 'no'),
170             $obj->info('exists', $to_parent),
171             $obj->{tree}->getmode($to_parent));
172             return if(! $ignore_status and (! $obj->info('exists', $to_parent) or $obj->{tree}->getmode($to_parent) eq 'open'));
173            
174             $obj->{tree}->add($new_path,
175             -itemtype => 'imagetext',
176             -data => $data,
177             -text => $obj->parse_text($data->{$obj->{textcolumn}}, $obj->{textcolumn}),
178             -style => $obj->{normal},
179             );
180            
181             $obj->{tree}->setmode($new_path, 'open')
182             if(defined $obj->{ptree}->{$iid});
183            
184            
185             &{$obj->{entry_create_cb}}($obj->{tree}, $new_path, $data)
186             if(defined $obj->{entry_create_cb} and ref $obj->{entry_create_cb} eq 'CODE');
187            
188             my $c = 1;
189             foreach my $field (@{$obj->{fields}}) {
190             $obj->{tree}->itemCreate( $new_path, $c++,
191             -text => $obj->parse_text($data->{$field}, $field),
192             -style => $obj->{normal},
193             );
194             }
195             push(@{$obj->{ListOfAllEntries}}, $new_path);
196             $obj->{Paths}->{$id} = $new_path;
197             return $new_path;
198             }
199            
200             # ------------------------------------------
201             sub move {
202             # ------------------------------------------
203             my $obj = shift || return error('No Object');
204             my $from_entry = $obj->id2path(shift) || return error('No From Id');
205             my $to_parent = shift || return error('No To Id');
206             my $data = shift;
207            
208             my $to_path = $obj->{Paths}->{int $to_parent} || $obj->{Paths}->{$to_parent} || $obj->id2path($to_parent);
209             my ($id, $pid) = $obj->id($from_entry);
210            
211            
212             my $did = $obj->dele($from_entry)
213             if($obj->info('exists',$from_entry));
214             my $nid = $obj->neu($id, $to_path, $data)
215             if($obj->info('exists',$to_path));
216            
217             $obj->{ListOfAllEntries} = $obj->rm_from_array($obj->{ListOfAllEntries}, $from_entry);
218             push(@{$obj->{ListOfAllEntries}}, $nid)
219             if($nid);
220            
221             return $nid;
222             }
223            
224             # ------------------------------------------
225             sub copy {
226             # ------------------------------------------
227             my $obj = shift || return error('No Object');
228             my $from_entry = shift || return error('No From Id');
229             my $to_parent = shift || return error('No To Id');
230             my $data = shift;
231            
232             my ($id, $pid) = $obj->id($from_entry);
233             my $to_entry = sprintf('%s:%d', $obj->{Paths}->{$to_parent}, $id);
234            
235             my $hl = $obj->{tree};
236            
237             return unless($obj->{tree}->infoExists($from_entry));
238             return unless($obj->{tree}->infoExists($to_entry));
239            
240             my @entry_args;
241             foreach ($hl->entryconfigure($from_entry)) {
242             push @entry_args, $_->[0] => $_->[4] if defined $_->[4];
243             }
244            
245             $hl->add($to_entry, @entry_args);
246             $hl->entryconfigure($to_entry, -data => $data)
247             if defined $data;
248            
249             foreach my $col (1 .. $hl->cget(-columns)-1) {
250             my @item_args;
251             foreach ($hl->itemConfigure($from_entry, $col)) {
252             push @item_args, $_->[0] => $_->[4] if defined $_->[4];
253             }
254             $hl->itemCreate($to_entry, $col, @item_args);
255             }
256             $obj->refresh_id($to_entry, $data);
257             push(@{$obj->{ListOfAllEntries}}, $to_entry);
258             $obj->{Paths}->{$id} = $to_entry;
259             return $to_entry;
260             }
261            
262             # ------------------------------------------
263             sub dele {
264             # ------------------------------------------
265             my $obj = shift || return error('No Object');
266             my $id = shift || return error('No Id');
267            
268             my $parent = $obj->info('parent', $id) || return;
269            
270             $obj->{tree}->deleteEntry($id);
271             $obj->{ListOfAllEntries} = $obj->rm_from_array($obj->{ListOfAllEntries}, $id);
272             $obj->{tree}->setmode($parent, 'none')
273             if( ! $obj->info('children', $parent) );
274            
275             my ($iid, $pid) = $obj->id($id);
276             delete $obj->{Paths}->{ $iid };
277            
278             return $id;
279             }
280            
281             # ------------------------------------------
282             sub refresh {
283             # ------------------------------------------
284             my $obj = shift || return error('No Object');
285             my $redraw = shift || $obj->Table_is_Change($obj->{last_refresh_time}, $obj->{table});
286             my $item = shift;
287            
288             $obj->Busy;
289            
290             $obj->debug('Refresh: redraw = %s, item = %s',
291             (defined $redraw ? $redraw : 'NONE'),
292             (defined $item ? $item : 'NONE')
293             );
294            
295             return if(! defined $redraw && ! defined $item);
296            
297             if(defined $redraw and $redraw) {
298             $obj->debug('Redraw! %s', $item);
299             @{$obj->{ListOfAllEntries}} = ();
300             $obj->{Paths} = {};
301             $obj->{tree}->delete('all');
302             }
303            
304             unless(defined $obj->{tree_buttons}) {
305             my $c = -1;
306             foreach my $name ($obj->{textcolumn}, @{$obj->{fields}}) {
307             $c++;
308             $obj->{tree_buttons}->{$name} = $obj->{tree}->ResizeButton(
309             -text => $obj->{colNames}->[$c] || $name,
310             -relief => 'flat',
311             -border => -2,
312             -pady => -10,
313             -padx => 10,
314             -widget => \$obj->{tree},
315             -column => $c,
316             );
317            
318             $obj->Advertise(sprintf("HB_%s",$name) => $obj->{tree_buttons}->{$name});
319            
320             $obj->{tree}->headerCreate($c,
321             -itemtype => 'window',
322             -widget => $obj->{tree_buttons}->{$name},
323             );
324            
325             $obj->{tree}->columnWidth($c, $obj->{columnWidths}->[$c])
326             if(defined $obj->{columnWidths}->[$c]);
327            
328             }
329             }
330            
331             $obj->{dbtree} = DBIx::Tree->new(
332             connection => $obj->{dbh},
333             sql => $obj->makeSql,
334             method => sub { $obj->make_tree_list(@_) },
335             columns => [$obj->{idx}, $obj->{textcolumn}, $obj->{parent_id}],
336             start_id => $obj->{start_id},
337             recursive => 0,
338             );
339            
340             $obj->{fieldtypes} = $obj->getFieldTypes
341             unless(defined $obj->{fieldtypes});
342            
343             $obj->{tree}->configure(-command => $obj->{command})
344             if(defined $obj->{command} and ref $obj->{command} eq 'CODE');
345            
346             $obj->remember();
347             $obj->list({
348             item => $item,
349             redraw => $redraw,
350             });
351            
352             $obj->{tree}->focus;
353             # $obj->select_entrys($obj->{FoundEntrys});
354            
355             if($obj->{zoom} and scalar @{$obj->{FoundEntrys}}) {
356             $obj->{zoom} = 0;
357             $obj->zoom();
358             }
359            
360             $obj->Unbusy;
361            
362             $obj->{last_refresh_time} = time;
363             }
364            
365             # ------------------------------------------
366             sub select_entrys {
367             # ------------------------------------------
368             my $obj = shift || return error('No Object');
369             $obj->{FoundEntrys} = shift || return $obj->{FoundEntrys};
370             $obj->color_all();
371             $obj->zoom if($obj->infozoom);
372            
373             my @found;
374             foreach my $id (@{$obj->{FoundEntrys}} ) {
375             my $entry = $obj->id2path($id);
376             push(@found, $entry)
377             if(defined $entry);
378             }
379             $obj->{FoundEntrys} = \@found;
380            
381             foreach (@found) {
382             $obj->to_parent_open($_);
383             $obj->color_row($_, $obj->{highlight});
384             }
385             my $entry = $found[0] || return;
386            
387             if($obj->info('exists', $entry)) {
388             $obj->{tree}->anchorSet($entry);
389             $obj->{tree}->selectionSet($entry);
390             $obj->{tree}->see($entry);
391             }
392             }
393            
394             # ------------------------------------------
395             sub see {
396             # ------------------------------------------
397             my $obj = shift || return error('No Object');
398             my $ids = shift || return debug('No Id in see()!');
399             my @ret;
400             $obj->{tree}->selectionClear();
401             undef $obj->{FoundEntrys};
402             foreach my $id ( split(/[^0-9A-Za-z]/, $ids) ) {
403             next unless($id);
404             my $entry = $obj->id2path($id) || next;
405             push(@{$obj->{FoundEntrys}}, $id);
406             $obj->debug('%s: = %s', $id, $entry);
407             $obj->to_parent_open($entry);
408             $obj->{tree}->selectionSet($entry);
409             push(@ret, $entry);
410             }
411            
412             my $last = $ret[0] or return error('No Id to see!');
413             $obj->{tree}->OpenCmd(1)
414             if($obj->{tree}->getmode(1) eq 'open');
415             $obj->{tree}->see($last);
416             $obj->{tree}->anchorSet($last);
417             $obj->color_row($last, $obj->{highlight});
418            
419             $obj->select_entrys( \@ret )
420             if(scalar @ret > 1);
421             return @ret;
422             }
423            
424            
425             # ------------------------------------------
426             sub color_row {
427             # ------------------------------------------
428             my $obj = shift || return error('No Object');
429             my $id = shift || return error('No Id');
430             my $color = shift || $obj->{normal};
431            
432             $id = $obj->id2path($id)
433             unless($id =~ /\:/);
434             return unless($obj->info('exists', $id));
435            
436             my $i = 0;
437             foreach ($obj->{textcolumn}, @{$obj->{fields}}) {
438             $obj->{tree}->itemConfigure($id, $i, -style => $color);
439             $i++;
440             }
441             }
442            
443             # ------------------------------------------
444             sub color_clear {
445             # ------------------------------------------
446             my $obj = shift || return error('No Object');
447             my $entrys = shift || $obj->{FoundEntrys} || return;
448             my $color = shift || $obj->{normal};
449            
450             foreach my $entry (sort @{$entrys}) {
451             $obj->color_row($entry, $color);
452             }
453             }
454            
455            
456             # ------------------------------------------
457             sub color_all {
458             # ------------------------------------------
459             my $obj = shift || return error('No Object');
460             my $color = shift || $obj->{normal};
461            
462             foreach my $entry (sort @{$obj->{ListOfAllEntries}}) {
463             $obj->color_row($entry, $color);
464             }
465             }
466            
467             # ------------------------------------------
468             sub remember {
469             # ------------------------------------------
470             my $obj = shift || return error('No Object');
471             my $rem = shift;
472             my $ret;
473             unless( $rem ) {
474             $ret->{status} = $obj->{OPEN};
475             $ret->{version} = $Version;
476             my $i = 0;
477             my $conf;
478             foreach my $spalte ($obj->{textcolumn}, @{$obj->{fields}}) {
479             push(@{$ret->{widths}}, $obj->{tree}->columnWidth($i++));
480             }
481             } else {
482             return $obj->debug('This configuration (V: %s) isn\'t compatible with this (V: %s). Ignoring.',
483             (defined $rem->{version} ? $rem->{version} : 'NoVersion'),
484             $Version)
485             if(! defined $rem->{version} or (defined $rem->{version} and $Version > $rem->{version}));
486            
487             $obj->{OPEN} = $obj->{tree}->{status} = $rem->{status}
488             if(defined $rem->{status});
489            
490             $obj->{widths} = $rem->{widths}
491             if(defined $rem->{widths});
492             }
493             return $ret;
494             }
495            
496             # ------------------------------------------
497             sub make_tree_list {
498             # ------------------------------------------
499             my $obj = shift || return error('No Object');
500             my %parms = @_;
501            
502             my @parent_ids = @{ $parms{parent_id} };
503            
504             my $treeval = '';
505             foreach (@parent_ids) {
506             $treeval .= "$_:";
507             }
508             $treeval .= $parms{id};
509             push(@{$obj->{ListOfAllEntries}}, $treeval);
510             }
511            
512             # ------------------------------------------
513             sub get_id {
514             # ------------------------------------------
515             my $obj = shift || return error('No Object');
516             my $w = shift || return error('No Widget!');
517             my $ev = $w->XEvent;
518             my $id = $w->nearest($ev->y);
519             $obj->{tree}->anchorSet($id);
520             $obj->{tree}->selectionClear();
521             $obj->{tree}->selectionSet($id);
522             my ($col, $col_nr) = $obj->x2col( $ev->x + $w->xview() );
523             my $wert = $w->itemCget($id, $col_nr, -text);
524             return ($id, $col, $col_nr, $wert);
525             }
526            
527             # ------------------------------------------
528             sub x2col {
529             # ------------------------------------------
530             my $obj = shift || return error('No Object');
531             my $x = shift;
532             my $c = 0;
533             my $von = 0;
534             foreach my $name ($obj->{textcolumn}, @{$obj->{fields}}) {
535             my $breite = $obj->{tree}->columnWidth( $c);
536             my $bis = $von + $breite;
537             return (($obj->{colNames}->[$c] || $name), $c)
538             if($x >= $von && $x <= $bis);
539             $von += $breite;
540             $c++;
541             }
542             }
543            
544             # ------------------------------------------
545             sub infozoom {
546             # ------------------------------------------
547             my $obj = shift || return error('No Object');
548             $obj->debug('Zoom is %s', ( $obj->{zoom} ? 'on' : 'off' ));
549             return $obj->{zoom};
550             }
551            
552             # ------------------------------------------
553             sub zoom {
554             # ------------------------------------------
555             my $obj = shift || return error('No Object');
556             return unless($obj->{FoundEntrys});
557             $obj->Busy;
558             $obj->{zoom} = ($obj->{zoom} ? undef : 1);
559             if($obj->{zoom}) {
560             foreach my $entry (sort @{$obj->{ListOfAllEntries}}) {
561             next unless($entry);
562             my $search = $entry;
563             $search =~ s/\:/\\:/sig;
564             unless(grep(/^$search/, @{$obj->{FoundEntrys}})) {
565             unless($obj->info('hidden', $entry)) {
566             $obj->{tree}->hide('entry', $entry);
567             push(@{$obj->{HiddenEntrys}}, $entry);
568             }
569             }
570             }
571             } else {
572             foreach my $entry (@{$obj->{HiddenEntrys}}) {
573             $obj->{tree}->show('entry', $entry)
574             if($obj->info('hidden', $entry));
575             }
576             @{$obj->{HiddenEntrys}} = qw//;
577             }
578             $obj->Unbusy;
579             }
580            
581            
582             # ------------------------------------------
583             sub makeSql {
584             # ------------------------------------------
585             my $obj = shift || return error('No Object');
586             my $sql;
587            
588             if($obj->{joiner}) {
589             $sql = sprintf('SELECT %s, %s, %s, %s FROM %s LEFT JOIN %s ON %s ORDER BY %s',
590             $obj->{idx}, $obj->{textcolumn},join(',', @{$obj->{fields}}), $obj->{parent_id},
591             $obj->{table}->[0], $obj->{table}->[1],
592             $obj->{joiner},
593             $obj->{textcolumn}
594             );
595             } else {
596             $sql = sprintf('select %s, %s, %s, %s from %s %s ORDER BY %s, %s',
597             $obj->{idx}, $obj->{textcolumn} ,join(',', @{$obj->{fields}}), $obj->{parent_id},
598             $obj->{table},
599             sprintf('WHERE %s in (%s)', $obj->{parent_id}, join(',', @{$obj->search_unique_ids($obj->{OPEN})} ) ),
600             $obj->{parent_id}, $obj->{idx}
601             );
602             }
603             $obj->debug('makeSql: %s', $sql)
604             if($obj->{debug});
605             return $sql;
606             }
607            
608            
609             # ------------------
610             sub search_unique_ids {
611             # ------------------
612             my $obj = shift || return error ('No Object!' );
613             my $ids = shift;
614             my $ret;
615            
616             foreach my $item (keys %$ids) {
617             foreach(split(':', $item)) {
618             $ret->{$_} = 1;
619             };
620             }
621             my @r = (0,$obj->{start_id}, keys %$ret);
622             return \@r;
623             }
624            
625            
626             # ------------------------------------------
627             sub getFieldTypes {
628             # ------------------------------------------
629             my $obj = shift or return warn("No object");
630             my $dbh = $obj->{dbh};
631             my $table = ref $obj->{table} ? $obj->{table}->[0] : $obj->{table};
632            
633             return $obj->{$table}->{fieldtypes}
634             if(defined $obj->{$table}->{fieldtypes});
635            
636             $obj->{$table}->{fieldtypes} = $dbh->selectall_hashref("show fields from $table", 'Field')
637             or return $obj->debug($dbh->errstr);
638            
639             return $obj->{$table}->{fieldtypes};
640             }
641            
642            
643             # ------------------------------------------
644             sub list {
645             # ------------------------------------------
646             my $obj = shift || return error('No Object');
647             my $arg = shift;
648            
649             my $item = $arg->{item};
650             my $redraw = $arg->{redraw};
651            
652             return if(defined $item and $obj->info('exists', $item) and $obj->info('children', $item));
653             my $idx = ( index($obj->{idx}, '.') ? ( split( '\.', $obj->{idx} ) )[-1] : $obj->{idx});
654             my $iname = $1 if($idx =~ /([a-z_]+)/si);
655             my $len = $1 if($obj->getFieldTypes->{$iname}->{Type} =~ /(\d+)/);
656            
657             if($DBIx::Tree::VERSION < 1) {
658             $obj->{dbtree}->do_query;
659             $obj->{dbtree}->tree;
660             } else {
661             $obj->{dbtree}->traverse;
662             }
663            
664             my $sql = $obj->makeSql;
665            
666             my $DATA = $obj->{dbh}->selectall_hashref( $sql, $idx)
667             or return error($obj->{dbh}->errstr);
668            
669             foreach my $id (sort @{$obj->{ListOfAllEntries}}) {
670             my ($item_id, $pid) = $obj->id($id);
671             next if(! $obj->{tree}->infoExists($pid) and $pid);
672             next if($obj->{tree}->infoExists($id));
673            
674             $obj->{Paths}->{$item_id} = $id;
675             my $row = $DATA->{$item_id} || $DATA->{sprintf("%0${len}d", $item_id)} || error('Error: No Data for %s', $item_id);
676             $obj->{tree}->add($id,
677             -itemtype => 'imagetext',
678             -data => $row,
679             -text => $obj->parse_text($row->{$obj->{textcolumn}}, $obj->{textcolumn}),
680             -style => $obj->{normal},
681             ) if($row);
682            
683             &{$obj->{entry_create_cb}}($obj->{tree}, $id, $row)
684             if(defined $obj->{entry_create_cb} and ref $obj->{entry_create_cb} eq 'CODE');
685            
686             my $c = 1;
687             foreach my $field (@{$obj->{fields}}) {
688             $obj->{tree}->itemCreate( $id, $c++,
689             -text => $obj->parse_text($row->{$field}, $field),
690             -style => $obj->{normal},
691             );
692             }
693             }
694            
695             # Draw Indicators
696             $obj->{tree}->autosetmode;
697            
698            
699             # Check is exists Ptree or Table is change, then reload
700             $sql = sprintf('select %s, %s from %s GROUP BY %s',
701             $obj->{idx}, $obj->{parent_id},
702             (ref $obj->{table} ? join(',', @{$obj->{table}}) : $obj->{table}),
703             $obj->{parent_id}
704             );
705            
706             $obj->debug($sql);
707             $obj->{ptree} = $obj->{dbh}->selectall_hashref($sql, $obj->{parent_id})
708             if($redraw);
709            
710             # Set the modes from every tree
711             foreach my $entry (@{$obj->{ListOfAllEntries}}) {
712             my ($id, $pid) = $obj->id($entry);
713            
714             if( defined $obj->{ptree}->{$id} and $obj->{tree}->getmode($entry) eq 'none') {
715             $obj->{tree}->setmode($entry, 'open');
716             }
717             if( defined $obj->{ptree}->{$id} and $obj->{tree}->getmode($entry) eq 'close') {
718             $obj->{tree}->setmode($entry, 'close');
719             }
720             if(! defined $obj->{ptree}->{$id}) {
721             $obj->{tree}->setmode($entry, 'none');
722             }
723             }
724            
725             }
726            
727             # ------------------------------------------
728             sub close_all {
729             # ------------------------------------------
730             my $obj = shift || return error('No Object');
731             return unless(ref $obj->{ListOfAllEntries} eq 'ARRAY');
732             foreach my $entry (sort @{$obj->{ListOfAllEntries}}) {
733             $obj->{tree}->close($entry);
734             }
735             }
736            
737             # ------------------------------------------
738             sub to_parent_open{
739             # ------------------------------------------
740             my $obj = shift || return error('No Object');
741             my $entry = shift || return error('No Entry!');
742             $entry = $obj->id2path($entry) || return error('This Entry <%s> is not exist!', $entry);
743            
744             my ($id, $pid) = $obj->id($entry);
745             my $path = $obj->{start_id};
746             foreach my $e (split(/\:/, $entry)) {
747             next if($e eq $obj->{start_id});
748             next if($e eq $id);
749             $path .= sprintf(':%d', $e);
750             $obj->{tree}->open($path)
751             if($obj->info('exists', $path));
752             }
753             }
754            
755             # ------------------------------------------
756             sub parse_text {
757             # ------------------------------------------
758             my $obj = shift;
759             my $text = shift || return ' ';
760             my $field = shift || return error('No FieldName!');
761             my $maxchars =
762             (ref $obj->{maxchars} eq 'HASH'
763             ? $obj->{maxchars}->{$field}
764             : $obj->{maxchars}
765             ) || 0;
766             $text = substr($text, 0, $maxchars).'...'
767             if($maxchars and length($text)>$maxchars);
768             $text =~ s/(\r|\n)//sig;
769             return $text;
770             }
771            
772             # ------------------------------------------
773             sub parent_id {
774             # ------------------------------------------
775             my $obj = shift || return error('No Object');
776             my $path = shift || return error('No Path');
777             my $parent = $obj->info('parent', $path)
778             or return error('Parent not found!');
779             return (split( /:/, $parent ))[-1];
780             }
781            
782            
783             # ------------------------------------------
784             sub id {
785             # ------------------------------------------
786             my $obj = shift || return error('No Object');
787             my $path = shift || return error('No Path');
788            
789            
790             return $path if(index($path, ':') == -1);
791             my @elms = split(':', $path);
792             my $id = pop @elms;
793             my $pid = join(':', @elms);
794             return ($id, $pid);
795             }
796            
797             # ------------------
798             sub childs {
799             # ------------------
800             my $obj = shift || return error ('No Object!' );
801             my ($i, $p) = $obj->id(shift);
802             my $id = int($i) || return error ('No Id!' );
803             $obj->debug('childs - Id: %s', $id);
804             my @ret;
805            
806             # Caching
807             return @{$obj->{children}->{$id}}
808             if(defined $obj->{children}->{$id});
809            
810             if(defined $obj->{ptree}->{$id}) {
811             my $sql = sprintf('select %s from %s where %s = %d',
812             $obj->{idx}, $obj->{table}, $obj->{parent_id}, $id);
813             my $chields = $obj->getSqlArray($sql);
814             foreach my $child (@{$chields}) {
815             push(@ret, $child->[0]);
816             my @ch = $obj->childs($child->[0])
817             if(defined $obj->{ptree}->{$child->[0]});
818             push(@ret, @ch);
819             }
820             }
821             # Caching
822             $obj->{children}->{$id} = \@ret;
823             return @ret;
824             }
825            
826            
827            
828             # ------------------------------------------
829             sub id2path {
830             # ------------------------------------------
831             my $obj = shift || return error('No Object');
832             my $id = shift || return debug('No ID');
833            
834             return $id
835             if($id =~ /\:/);
836            
837             return $obj->{Paths}->{$id}
838             if(defined $obj->{Paths}->{$id} and $obj->{Paths}->{$id} =~ /\:/ and $obj->{Paths}->{$id} ne $obj->{start_id});
839            
840             $id = int($id);
841            
842             my @tree;
843             my $parent_id = $obj->sqlv("select %s from %s where %s = '%s'",
844             $obj->{parent_id},
845             (ref $obj->{table} ? join(',', @{$obj->{table}}) : $obj->{table}),
846             $obj->{idx}, $id);
847             return error('No found!')
848             unless(defined $parent_id);
849            
850             unshift(@tree, $parent_id, $id);
851            
852            
853             my $maxdeep;
854             while($parent_id) {
855             last if($maxdeep++ >= 10);
856             my $vater_parent_id = $obj->sqlv('select %s from %s where %s = "%s"',
857             $obj->{parent_id}, $obj->{table}, $obj->{idx}, $parent_id) || last;
858             unshift(@tree, $vater_parent_id);
859             $parent_id = $vater_parent_id;
860             }
861            
862             # Cache this Information
863             $obj->{Paths}->{$id} = join(':', @tree);
864             return $obj->{Paths}->{$id};
865            
866             }
867            
868             # ------------------------------------------
869             sub listEntrys {
870             # ------------------------------------------
871             my $obj = shift;
872             return $obj->{ListOfAllEntries};
873             }
874            
875             # ------------------------------------------
876             sub info {
877             # ------------------------------------------
878             my $obj = shift or return error("No object");
879             my $typ = shift or return error("No Type");
880             my $entry = shift;
881            
882             # $obj->debug('info: %s - %s', $typ, $entry);
883            
884             if($typ =~ /^(selection|anchor|dragsite|dropsite)$/si) {
885             my @ids = $obj->{tree}->info($typ);
886             return \@ids;
887             }
888            
889             if($entry and $entry !~ /\:/) {
890             $entry = $obj->id2path($entry)
891             or return error('Can\'t find <%s> in Paths!', $entry);
892             }
893            
894             return error('Can\'t find Id: %s', $entry)
895             if($typ ne 'exists' and ! $obj->{tree}->info('exists', $entry));
896             my $answ = $obj->{tree}->info($typ, $entry);
897             # $obj->debug('info: Answer = <%s>', $answ);
898             return $answ;
899             }
900            
901            
902             # ------------------------------------------
903             sub getSqlArray {
904             # ------------------------------------------
905             my $obj = shift or return error("No object");
906             my $sql = shift or return error('No Sql');
907             my $dbh = $obj->{dbh};
908            
909             $obj->debug($sql);
910             my $sth = $dbh->prepare($sql) or return error("$DBI::errstr - $sql");
911             $sth->execute or return error("$DBI::errstr - $sql");
912             return $sth->fetchall_arrayref;
913             }
914            
915             # ------------------------------------------
916             sub Table_is_Change {
917             # ------------------------------------------
918             my $obj = shift or return error("No object");
919             my $lasttime = shift || $obj->{last_refresh_time}; # No last time, first request!
920             my $table = shift || $obj->{table} || $obj->{table}->[0];
921            
922             my $dbh = $obj->{dbh};
923             my $ret = 0;
924            
925             my $data = $dbh->selectall_hashref(sprintf("SHOW TABLE STATUS LIKE '%s'", $table),'Name')
926             or return $obj->debug($dbh->errstr);
927            
928             my $unixtime = $obj->sqlv("select UNIX_TIMESTAMP('%s')", $data->{$table}->{Update_time})
929             if(defined $data->{$table}->{Update_time});
930            
931             $obj->{last_refresh_time} = time;
932            
933             if(defined $unixtime and $unixtime > $lasttime) {
934             return 1;
935             }
936             }
937            
938             # ------------------------------------------
939             sub rm_from_array {
940             # ------------------------------------------
941             my $obj = shift || return error('No Object');
942             my $arr = shift || return error('No Array');
943             my $id = shift || return error('No Id');
944             $obj->debug('rm_from_array - Arr: %s, Id: %s', $arr, $id);
945             my @new_array = grep(!/$id/, @$arr);
946            
947             return \@new_array;
948             }
949            
950             # ------------------
951             sub sqlv {
952             # ------------------
953             my $obj = shift || return error ('No Object!' );
954             my $sql = sprintf(shift, @_) || return error ('No Sql' );
955            
956             $obj->debug($sql);
957             return $obj->getSqlArray($sql)->[0][0];
958             }
959            
960            
961            
962             # ------------------------------------------
963             sub debug {
964             # ------------------------------------------
965             my $obj = shift;
966             return unless($obj->{debug});
967             my ($package, $filename, $line, $subroutine, $hasargs,
968             $wantarray, $evaltext, $is_require, $hints, $bitmask) = caller(1);
969             print color 'green';
970             printf '#%d: ', $line;
971             printf @_ if(scalar @_);
972             print "\n";
973             print color 'reset';
974             }
975            
976             # ------------------------------------------
977             sub error {
978             # ------------------------------------------
979             my $msg = shift;
980             my ($package, $filename, $line, $subroutine, $hasargs,
981             $wantarray, $evaltext, $is_require, $hints, $bitmask) = caller(1);
982             my $error = sprintf("ERROR in %s %s #%d: <%s>\n",
983             (defined $package ? $package : 'nopackage'),
984             (defined $subroutine ? $subroutine : 'nosub'),
985             (defined $line ? $line : 'noline'),
986             (defined $msg ? sprintf($msg, @_) : 'no message')
987             );
988             print color 'bold red';
989             print $error;
990             print color 'reset';
991             return undef;
992             }
993            
994            
995             1;
996            
997            
998             =head1 NAME
999            
1000             Tk::DBIx::Tree - Megawidget to display a table column in a tree.
1001            
1002             =head1 SYNOPSIS
1003            
1004             use Tk;
1005             use Tk::DBIx::Tree;
1006            
1007             my $top = MainWindow->new;
1008             my $tkdbi = $top->DBITree(
1009             -dbh => $dbh,
1010             -table => 'Inventory',
1011             -textcolumn => 'name',
1012             -idx => 'id',
1013             -columnWidths => [undef, undef, undef, 150],
1014             -fields => [qw(changed_by changed_at descr)],
1015             -parent_id => 'parent_id',
1016             -start_id => 1,
1017             -maxchars => { descr => 25 },
1018             )->pack(-expand => 1,
1019             -fill => 'both');
1020            
1021             MainLoop;
1022            
1023             =head1 DESCRIPTION
1024            
1025             This is a megawidget to display a sql statement from your database in a tree view
1026             widget. When you've got one of those nasty self-referential tables that you
1027             want to bust out into a tree, this is the module to check out.
1028            
1029             =head1 WIDGET-SPECIFIC OPTIONS
1030            
1031             =head2 -dbh => $ref_on_database_handle
1032            
1033             A database handle, this will return an error if it is'nt defined.
1034            
1035             =head2 -debug => [I<0>|1]
1036            
1037             This is a switch to turn on debug output to the standard console (STDOUT)
1038            
1039             =head2 -table => 'tablename'
1040            
1041             The table to display.
1042            
1043             =head2 -idx => 'index_column'
1044            
1045             The index column from the table.
1046            
1047             =head2 -fields => [col0, col1, col2, ...]
1048            
1049             List of additional fields to display.
1050            
1051             =head2 -colNames => [col0, col1, col2, ...]
1052            
1053             List of alternative names for every column. This will display on header.
1054            
1055             =head2 -where => 'WHERE foo == 1, ...'
1056            
1057             Additional where statement for choice rows in table.
1058            
1059             =head2 -textcolumn => colname
1060            
1061             The name of the column to be displayed in the tree..
1062            
1063             =head2 -start_id => integer
1064            
1065             The id, where the widget will start to create the tree. Default is 1.
1066            
1067             =head2 -columnWidths => [colWidth_0, colWidth_1, colWidth_2, ...]
1068            
1069             Default field column width.
1070            
1071             =head2 -highlight => I<[-foreground => 'blue']>
1072            
1073             Style for founded Entries.
1074            
1075             =head2 -normal => I<[-foreground => 'black']>
1076            
1077             Default style for Entries.
1078            
1079             =head2 -maxchars => number or {col1 =number}
1080            
1081             Maximum number of characters to be displayed within the cells. Global
1082             validity or set only for named columns.
1083             I.E.:
1084            
1085             -maxchars => {
1086             descr => 25,
1087             name => 10,
1088             },
1089             # or ....
1090             -maxchars => 25, # global for all fields
1091            
1092            
1093             =head2 -time_column => $name_from_time_column
1094            
1095             Maximum number of characters to be displayed within the cells. Global
1096             validity or set only for named columns.
1097             I.E.:
1098            
1099             -maxchars => {
1100             descr => 25,
1101             name => 10,
1102             },
1103             # or ....
1104             -maxchars => 25, # global for all fields
1105            
1106             =head1 METHODS
1107            
1108             These are the methods you can use with this Widget.
1109            
1110             =head2 $DBITree->refresh('reload');
1111            
1112             Refresh the tree. if you call this method with the parameter reload
1113             then this will reload the table from database. If you call this without parameter, then
1114             look this widget is the table changed (update date) at the last refresh. If this true, then
1115             load this the complete table and redraw the tree.
1116            
1117             =head2 $DBITree->refresh_id( I, I );
1118            
1119             This will refresh (delete -> new) a Tree item.
1120            
1121             =head2 $DBITree->copy( I, I, I );
1122            
1123             Copy an entry (entry) to a parent branch (to_parent_entry) with data (data);
1124            
1125             =head2 $DBITree->move( I, I, I );
1126            
1127             Move an entry (from_entry) to a parent branch (to_parent_entry) with data (data);
1128            
1129             =head2 $DBITree->dele( I );
1130            
1131             Delete a entry.
1132            
1133             =head2 $DBITree->neu( I, I, I );
1134            
1135             Create a entry.
1136            
1137             =head2 $DBITree->close_all;
1138            
1139             close all tree branches.
1140            
1141             =head2 $DBITree->info('anchor, bbox, children, B, dragsite, dropsite ...', $id);
1142            
1143             This is a wrapper to the HList Method ->info. The default method is info('data', ...).
1144             Please read the manual from Tk::HList.
1145            
1146             =head2 $DBITree->id2path(I);
1147            
1148             This returns the path for given id.
1149            
1150             =head2 $DBITree->ListEntrys;
1151            
1152             This returnd a sorted ref array with all entrys in the tree.
1153            
1154             =head2 $DBITree->select_entrys([en1, en2, en3, ...]);
1155            
1156             This returns a sorted ref array with all selected entries
1157             in the tree or you can set an array of selected entries.
1158             Also you can use only the id's, i.e.:
1159            
1160             $dbitree->select_entrys([qw/1:2 1:3 1:4/]);
1161            
1162             # or ...
1163            
1164             $dbitree->select_entrys([qw/2 3 4/]);
1165            
1166             These is friendly if you use i.e. a statement 'select id from table where foo == bla'
1167             and you have only the id's without the pathinformation. Tk::DBIx::Tree know, select only
1168             the entries have at last position this id in path.
1169            
1170             =head2 $DBITree->zoom;
1171            
1172             Shrink or unshrink tree to display only founded entries.
1173            
1174             =head2 $DBITree->infozoom;
1175            
1176             Returnd true if zoom active.
1177            
1178             =head2 $DBITree->color_all([style]);
1179            
1180             Set all entries to normal style without parameters.
1181             You can put a new Style to all entries.
1182            
1183             i.e:
1184            
1185             $DBITree->color_clear([qw/1 2 3/], [-background => 'gray50']);
1186            
1187             =head2 $DBITree->color_clear([entrys], style);
1188            
1189             Remove all higlighted styles from the functions see and select_entrys.
1190             if you don't give entrys, tree take the internal foundentrys.
1191             if you don't give style, tree take the normal style.
1192            
1193             i.e:
1194            
1195             $DBITree->color_clear([-background => 'gray50']);
1196            
1197            
1198             =head2 $DBITree->get_id;
1199            
1200             select the row under mouseposition and returnd following parameters.
1201            
1202             =over 4
1203            
1204             =item path - The path from the entry under mouseposition.
1205            
1206             =item col - Column name under mouseposition.
1207            
1208             =item path - Column number under mouseposition.
1209            
1210             =item value - Cell value under mouseposition.
1211            
1212             =back
1213            
1214             =head2 $DBITree->childs($item);
1215            
1216             Return an array with paths from childs for $item, include $item.
1217            
1218             =head2 $DBITree->parent_id($path);
1219            
1220             Return the actually id from the parentree (only the integer id, not the hole path)
1221            
1222             =head2 $DBITree->see($id);
1223            
1224             Jump to id (only database id)
1225            
1226             =head2 $DBITree->remember( $hash );
1227            
1228             This method is very useful, when you want to remember the last tree status
1229             and column widths for the resize button. This returns a ref hash with following
1230             keys, if this call is done without parameters.
1231            
1232             =over 4
1233            
1234             =item widths - a ref array including the width of each column.
1235            
1236             =item stats - a ref hash with status information(open close none) for each entry.
1237            
1238             =back
1239            
1240             You can give an old Hash (may eval-load at program start) and the tree
1241             remembers this status.
1242            
1243             I.E.:
1244            
1245             $tree->rembember( $tree->rembember );
1246            
1247             # or ...
1248            
1249             $tree->remember( {
1250             status => {
1251             '0:1' ='open',
1252             '0:1:2' ='close',
1253             ...
1254             },
1255             widths =[165, 24, 546],
1256             } );
1257            
1258             =head1 CALLBACKS
1259            
1260             =head2 -command => sub{ ... }
1261            
1262             Callback on TreeWidget at browsing.
1263            
1264             =head2 -entry_create_cb => sub{ ... }
1265            
1266             Callback if an entry created. The routine have 2 parameters:
1267            
1268             =over 4
1269            
1270             =item entry - a ref to created entry
1271            
1272             =item data - a ref hash with row information.
1273            
1274             =back
1275            
1276             i.e;
1277            
1278             -entry_create_cb => sub{
1279             my($w, $path, $row) = @_;
1280             if(exists $DOC->{ $row->{id} } and exists $EVENT->{ $row->{id} } ) {
1281             $w->entryconfigure($path, -image => $pics{'icon_document_event'});
1282             }
1283             },
1284            
1285             =head2 -opencmd => sub{ ... }
1286            
1287             Callback on TreeWidget if this entry activate(from User or Automatic) to open.
1288             if this return false, then Tree doesn't refresh the tree.
1289             i.E.
1290             -opencmd => sub{ &display_prj_items( @_ ) },
1291            
1292            
1293             =head2 -closecmd => sub{ ... }
1294            
1295             Callback on TreeWidget if this entry activate(from User or Automatic) to close.
1296            
1297            
1298             =head1 ADVERTISED WIDGETS
1299            
1300             =head2 'tree' => Tree-Widget
1301            
1302             This is a normal Tree widget. I.e.:
1303            
1304             $DBITree->Subwidget('tree')->configure(
1305             -background => 'gray50',
1306             };
1307            
1308             =head2 'HB_' => ResizeButton-Widget
1309            
1310             This is a (Resize)Button widget.
1311            
1312             =head1 CHANGES
1313            
1314             $Log: Tree.pm,v $
1315             Revision 1.2 2003/11/06 17:55:56 xpix
1316             ! bugfixes in refresh_id
1317             * not hudge load for tree
1318              
1319             Revision 1.1 2003/10/24 10:46:28 xpix
1320             * new Name for CPAN
1321             * new Parser for tree, at ths time is possible only read a one Tree
1322            
1323             !! new cvs name !!
1324              
1325             Revision 1.11 2003/08/13 12:58:13 xpix
1326             * colored debug output
1327             * new method childs, output an array with complete paths from childs
1328             * new methods neu, move, dele to transfering from entrys
1329             * new option maxchars
1330             ! many, many bug fixes
1331              
1332             Revision 1.8 2003/07/18 16:14:15 xpix
1333             ! Fehler im Table_is_Change algo, fix
1334             ! Komplett refresh bei TreeStatusaenderung
1335             ! to_parent_open macht jetzt wirklich nur die Vaeter und nicht noch den Sohn auf ;-)
1336             ! unit. value in Form.pm
1337              
1338             Revision 1.10 2003/07/17 14:59:54 xpix
1339             ! many little bugfixes
1340              
1341             Revision 1.3 2003/06/24 16:38:44 xpix
1342             * add symbolic to cvs
1343             * new loking mechanism in Form.pm
1344              
1345             Revision 1.9 2003/06/23 16:15:22 xpix
1346             ! cvs error
1347              
1348             Revision 1.8 2003/06/18 15:31:47 xpix
1349             * new methods: copy, move, neu ... dele. This will work only on the Tree Widget (not in table)
1350             * change a little bit the docu
1351              
1352             Revision 1.7 2003/06/16 12:58:01 xpix
1353             ! No Error, if the id ot exists in selct_entrys
1354              
1355             Revision 1.6 2003/05/23 13:47:46 xpix
1356             ! No debug if debug = 0
1357              
1358             Revision 1.5 2003/05/20 13:51:50 xpix
1359             * add field parent_id to data entry
1360              
1361             Revision 1.4 2003/05/11 16:33:47 xpix
1362             * new option -colNames
1363             * new option -entry_create_cb
1364             * new option -higlight
1365             * new option -normal
1366             * new method info
1367             * new method infozoom
1368             * new method color_all
1369             * new method get_id
1370             ! much bugfixes
1371             * better select_entrys (without pathinformation)
1372              
1373             Revision 1.3 2003/05/05 16:02:06 xpix
1374             * correct the documentation and write a little more ;-)
1375            
1376             Revision 1.2 2003/05/04 23:38:25 xpix
1377             ! bug in make_tree_list
1378            
1379             Revision 1.1 2003/05/04 20:52:13 xpix
1380             * New Widget for display a table in a tree
1381            
1382             =head1 AUTHOR
1383            
1384             Copyright (C) 2003 , Frank (xpix) Herrmann. All rights reserved.
1385            
1386             http://www.xpix.de
1387            
1388             This program is free software; you can redistribute it and/or