ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/kgsueme/bin/kgsueme
Revision: 1.6
Committed: Thu May 29 03:11:56 2003 UTC (21 years ago) by elmex
Branch: MAIN
Changes since 1.5: +96 -12 lines
Log Message:
Added GnomeCanvas for board graphics

File Contents

# User Rev Content
1 elmex 1.6 #!/usr/bin/perl -I../lib/
2 pcg 1.1
3 elmex 1.6 #use PApp::Util qw(dumpval); # debug only
4 pcg 1.1
5     use Gtk;
6     use Gtk::Gdk;
7 elmex 1.6 use Gtk;
8     #use Gtk::Gdk::ImlibImage;
9     use Gnome;
10     init Gnome "kgsueme";
11    
12 pcg 1.1
13     use KGS::Protocol;
14     use KGS::Listener::Debug;
15    
16     use IO::Socket::INET;
17    
18     use Errno;
19    
20     init Gtk;
21    
22 pcg 1.3 $HACK = 1; # do NEVER enable. ;)
23    
24 pcg 1.2 our $config;
25    
26 pcg 1.1 {
27     use Storable ();
28     use Scalar::Util ();
29    
30     my $staterc = "$ENV{HOME}/.kgsueme";
31    
32     my $state = -r $staterc ? Storable::retrieve($staterc) : {};
33     my @widgets;
34    
35 pcg 1.2 $config = $state->{config} ||= {};
36    
37 pcg 1.1 # grr... more gtk+ brokenness
38     my %get = (
39     hpane_position => sub { ($_[0]->children)[0]->allocation->[2] },
40     vpane_position => sub { ($_[0]->children)[0]->allocation->[3] },
41     window_size => sub { [ @{$_[0]->allocation}[2,3] ] },
42     #window_pos => sub { die PApp::Util::dumpval [ $_[0]->get_root_origin ] },
43     clist_column_widths => sub {
44     $_[0]{column_widths};
45     },
46     );
47    
48     my %set = (
49     hpane_position => sub { $_[0]->set_position($_[1]) },
50     vpane_position => sub { $_[0]->set_position($_[1]) },
51     window_size => sub { $_[0]->set_default_size(@{$_[1]}) },
52     #window_pos => sub { $_[0]->set_uposition(@{$_[1]}) if @{$_[1]} },
53     clist_column_widths => sub {
54     my ($w, $v) = @_;
55     $v->[$_] && $w->set_column_width($_, $v->[$_]) for 0..$#$v;
56     $w->{column_widths} = $v;
57     $w->signal_connect(resize_column => sub { $v->[$_[1]] = $_[2]; });
58     },
59     );
60    
61     sub state {
62     my ($widget, $class, $instance, %attr) = @_;
63    
64     while (my ($k, $v) = each %attr) {
65     my ($set, $get) = $k =~ /=/ ? split /=/, $k : ($k, $k);
66     $v = $state->{$class}{"*"}{$get} if exists $state->{$class}{"*"}{$get};
67     $v = $state->{$class}{$instance}{$get} if exists $state->{$class}{$instance}{$get};
68     $set{$get} ? $set{$get}->($widget, $v) : $widget->set($set => $v);
69     }
70    
71     $widget = [$widget, $class, $instance, \%attr];
72     Scalar::Util::weaken $widget->[0];
73    
74     @widgets = (grep $_->[0], @widgets, $widget);
75     }
76    
77     sub save_state {
78     for (@widgets) {
79     if ($_->[0]) {
80     my ($widget, $class, $instance, $attr) = @$_;
81     while (my ($k, $v) = each %$attr) {
82     my ($set, $get) = $k =~ /=/ ? split /=/, $k : ($k, $k);
83     $v = $get{$get} ? $get{$get}->($widget) : $widget->get($get);
84    
85     $state->{$class}{"*"}{$get} = $v;
86     $state->{$class}{$instance}{$get} = $v;
87     }
88     }
89     ::status("save_state", "layout saved");
90     }
91    
92     Storable::nstore($state, $staterc);
93     }
94     }
95    
96     # make a clist unselectable
97     sub clist_autosort {
98     my $w = shift;
99     my ($c, $o) = (-1);
100     for (0..$w->columns-1) {
101     $w->signal_connect(click_column => sub {
102     if ($_[1] != $c) {
103     $c = $_[1];
104     $o = 0;
105     } else {
106     $o = !$o;
107     }
108     $w->set_sort_column($c);
109     $w->set_sort_type($o ? "descending" : "ascending");
110     $w->sort;
111     });
112     }
113    
114     }
115    
116     {
117     my $main = new kgsueme;
118    
119     my %context_id;
120    
121     sub status {
122     my ($type, $text) = @_;
123    
124     $main->{status}->pop($context_id{$type}) if $context_id{$type};
125     $main->{status}->push($context_id{$type} ||= $main->{status}->get_context_id($type), $text) if $text;
126     }
127     }
128    
129     main Gtk;
130    
131     #############################################################################
132    
133     package kgsueme;
134    
135     use base KGS::Listener;
136    
137     sub new {
138     my $self = shift;
139     $self = $self->SUPER::new(@_);
140    
141     $self->{conn} = new KGS::Protocol;
142    
143     KGS::Listener::Debug->new->listen($self->{conn}); #d# debug only :)
144    
145     $self->listen($self->{conn});
146    
147 pcg 1.4 $self->{roomlist} = new roomlist conn => $self->{conn};
148    
149 pcg 1.1 $self->{window} = new Gtk::Window 'toplevel';
150     $self->{window}->set_title('kgsueme');
151     ::state $self->{window}, "main::window", undef, window_size => [400, 100];
152     $self->{window}->signal_connect(delete_event => sub { main_quit Gtk });
153    
154     $self->{window}->add(my $vbox = new Gtk::VBox);
155    
156 pcg 1.4 $vbox->pack_start(($buttonbox = new Gtk::HButtonBox), 0, 1, 0);
157     $buttonbox->set_spacing(0);
158 pcg 1.1
159 pcg 1.2 my $button = sub {
160 pcg 1.4 $buttonbox->add(my $button = new Gtk::Button $_[0]);
161 pcg 1.2 signal_connect $button clicked => $_[1];
162     };
163 pcg 1.1
164 pcg 1.4 $button->("Login", sub { $self->login; });
165     $button->("Roomlist", sub { $self->{roomlist}->show; });
166 pcg 1.2 $button->("Save Config & Layout", sub { ::save_state });
167 pcg 1.4 $button->("Quit", sub { main_quit Gtk });
168 pcg 1.1
169     $vbox->pack_start((my $hbox = new Gtk::HBox), 0, 1, 0);
170    
171     $hbox->add(new Gtk::Label "Login");
172    
173     $hbox->add($self->{login} = new_with_max_length Gtk::Entry 12);
174 pcg 1.2 $self->{login}->set_text($::config->{login});
175 pcg 1.3
176     if ($::HACK) {
177     $self->{login}->signal_connect(activate => sub {
178     $self->{conn}{name} = $self->{login}->get_text;
179     });
180     }
181 pcg 1.1
182     $hbox->add(new Gtk::Label "Password");
183 pcg 1.2 $hbox->add($self->{password} = new Gtk::Entry);
184     $self->{password}->set_visibility(0);
185 pcg 1.1
186     $vbox->pack_start(($self->{status} = new Gtk::Statusbar), 0, 1, 0);
187    
188     $self->{window}->show_all;
189    
190     $self;
191     }
192    
193     sub login {
194     my ($self) = @_;
195    
196 pcg 1.2 $self->{conn}->disconnect;
197    
198 pcg 1.1 # initialize new socket and connection
199     my $sock = new IO::Socket::INET PeerHost => "kgs.kiseido.com", PeerPort => "2379"
200     or die;
201    
202     $sock->blocking(1);
203     $self->{conn}->handshake($sock);
204     $sock->blocking(0);
205    
206     my $input; $input = input_add Gtk::Gdk fileno $sock, "read", sub {
207     # this is dorked
208     my $buf;
209     if (0 >= sysread $sock, $buf, 16384
210     and !$!{EINTR} and !$!{EAGAIN}) {
211     input_remove Gtk::Gdk $input;
212     $self->event_disconnect;
213     }
214     $self->{conn}->feed_data($buf);
215     };
216    
217     # now login
218 pcg 1.2 $self->{conn}->login($self->{login}->get_text, $self->{password}->get_text);
219 pcg 1.1 }
220    
221     sub inject_login {
222     my ($self, $msg) = @_;
223    
224     ::status("login", "logged in as '$self->{conn}{name}' with status '$msg->{result}'");
225 pcg 1.2 $::config->{login} = $self->{conn}{name};
226 pcg 1.1
227 pcg 1.4 if ($msg->{success}) {
228 pcg 1.5 warn "hiya\n";
229 pcg 1.4 for (keys %{$::config->{rooms}}) {
230 pcg 1.5 warn "hiya $_\n";
231 pcg 1.4 $self->{roomlist}->join_room($_);
232     }
233     }
234    
235     warn PApp::Util::dumpval($::config);
236 pcg 1.1 }
237    
238     sub event_disconnect { }
239    
240     #############################################################################
241    
242     package roomlist;
243    
244     use base KGS::Listener::Roomlist;
245    
246     sub new {
247     my $self = shift;
248     $self = $self->SUPER::new(@_);
249    
250     $self->listen($self->{conn});
251    
252     $self->{window} = new Gtk::Window 'toplevel';
253     $self->{window}->set_title('KGS Rooms');
254     ::state $self->{window}, "roomlist::window", undef, window_size => [400, 200];
255    
256 pcg 1.5 $self->{window}->signal_connect(delete_event => sub { $self->{window}->hide });
257 pcg 1.1
258     $self->{window}->add(my $vbox = new Gtk::VBox);
259    
260     $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
261     $sw->set_policy("automatic", "always");
262    
263     $sw->add($self->{roomlist} = new_with_titles Gtk::CList "Group", "Room Name", "Users", "Games", "Flags", "Channel");
264     $self->{roomlist}->set_selection_mode('multiple');
265     ::clist_autosort $self->{roomlist};
266     ::state $self->{roomlist}, "roomlist::roomlist", undef, clist_column_widths => [20, 200];
267    
268     $self->{roomlist}->signal_connect(select_row => sub {
269     my $room = $self->{roomlist}->get_row_data($_[1])
270     or return;
271     $self->{roomlist}->unselect_all;
272 pcg 1.4 $self->join_room($room->{channel});
273 pcg 1.1 });
274    
275     $self;
276     }
277    
278 pcg 1.4 sub join_room {
279     my ($self, $channel) = @_;
280    
281     $self->{room}{$channel} ||= room->new(channel => $channel, conn => $self->{conn}, users => {});
282     $self->{room}{$channel}->join;
283     }
284    
285 pcg 1.1 sub show {
286     my ($self, $msg) = @_;
287    
288     $self->msg(list_rooms => group => $_) for 0..5; # fetch all room names (should not!)
289     $self->{window}->show_all;
290     }
291    
292     sub event_update {
293     my ($self) = @_;
294    
295     $self->{event_update} ||= Gtk->timeout_add(200, sub {
296     my $l = $self->{roomlist};
297    
298     $l->freeze;
299     my $pos = $l->get_vadjustment->get_value;
300     $l->clear;
301    
302     my $row = 0;
303     for (values %{$self->{rooms}}) {
304     $l->append($_->{group}, $_->{name}, $_->{users}, $_->{games}, $_->{flags}, $_->{channel});
305     $l->set_row_data($row++, $_);
306     }
307     $l->sort;
308     $l->get_vadjustment->set_value($pos);
309     $l->thaw;
310    
311     delete $self->{event_update};
312     0;
313     });
314     }
315    
316     #############################################################################
317    
318     package room;
319    
320     use base KGS::Listener::Room;
321    
322     sub new {
323     my $self = shift;
324     $self = $self->SUPER::new(@_);
325    
326     $self->listen($self->{conn});
327    
328     $self->{window} = new Gtk::Window 'toplevel';
329     $self->{window}->set_title("KGS Room $self->{name}");
330     ::state $self->{window}, "room::window", $self->{name}, window_size => [600, 400];
331    
332     $self->{window}->signal_connect(delete_event => sub { $self->part });
333    
334     $self->{window}->add(my $hpane = new Gtk::HPaned);
335     ::state $hpane, "room::hpane", $self->{name}, hpane_position => 200;
336    
337     $hpane->add(my $vpane = new Gtk::VPaned);
338     ::state $vpane, "room::vpane", $self->{name}, vpane_position => 200;
339    
340     $vpane->add(my $sw = new Gtk::ScrolledWindow);
341     $sw->set_policy("automatic", "always");
342    
343     $sw->add($self->{gamelist} = new_with_titles Gtk::CList "T", "Black", "White", "Rules", "Notes");
344     ::clist_autosort $self->{gamelist};
345     ::state $self->{gamelist}, "room::gamelist", $self->{name}, clist_column_widths => [20, 120, 120, 120];
346    
347     $self->{gamelist}->signal_connect(select_row => sub {
348     my $game = $self->{gamelist}->get_row_data($_[1])
349     or return;
350     $self->{game}{$game->{channel}} ||= new game %$game, conn => $self->{conn};
351     $self->{game}{$game->{channel}}->join;
352     $self->{gamelist}->unselect_all;
353     });
354    
355     $vpane->add(my $vbox = new Gtk::VBox);
356    
357     $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
358     $sw->set_policy("automatic", "always");
359    
360     $sw->add($self->{text} = new Gtk::Text);
361    
362     $vbox->pack_start(($self->{entry} = new Gtk::Entry), 0, 1, 0);
363     $self->{entry}->signal_connect(activate => sub {
364     my $text = $self->{entry}->get_text;
365     $self->say($text) if $text =~ /\S/;
366     $self->{entry}->set_text("");
367     });
368    
369     $hpane->add(my $sw = new Gtk::ScrolledWindow);
370     $sw->set_policy("automatic", "always");
371    
372     $sw->add($self->{userlist} = new_with_titles Gtk::CList "User", "Rank", "Flags");
373     ::clist_autosort $self->{userlist};
374     ::state $self->{userlist}, "room::userlist", $self->{name}, clist_column_widths => [120, 30];
375    
376     $self;
377     }
378    
379     sub event_update {
380     my ($self) = @_;
381    
382     $self->{event_update} ||= Gtk->timeout_add(200, sub {
383     my $l = $self->{userlist};
384    
385     $l->freeze;
386     my $pos = $l->get_vadjustment->get_value;
387     $l->clear;
388    
389     my $row = 0;
390     for (values %{$self->{users}}) {
391     $l->append($_->{name});
392     $l->set_row_data($row++, $_);
393     }
394     $l->sort;
395     $l->get_vadjustment->set_value($pos);
396     $l->thaw;
397    
398     delete $self->{event_update};
399     0;
400     });
401     }
402    
403     sub event_update_games {
404     my ($self) = @_;
405    
406     $self->{event_update_games} ||= Gtk->timeout_add(200, sub {
407     my $l = $self->{gamelist};
408    
409     $l->freeze;
410     my $pos = $l->get_vadjustment->get_value;
411     $l->clear;
412    
413     my $row = 0;
414     for (values %{$self->{games}}) {
415     $l->append($_->type, $_->user0, $_->user1, $_->rules, $_->notes);
416     $l->set_row_data($row++, $_);
417     }
418     $l->sort;
419     $l->get_vadjustment->set_value($pos);
420     $l->thaw;
421    
422     delete $self->{event_update_games};
423     0;
424     });
425     }
426    
427     sub join {
428     my ($self) = @_;
429     $self->SUPER::join;
430    
431     $self->{window}->show_all;
432     }
433    
434     sub part {
435     my ($self) = @_;
436     $self->SUPER::part;
437    
438 pcg 1.4 delete $::config->{rooms}{$self->{channel}};
439 pcg 1.1 $self->{window}->hide_all;
440     $self->event_update;
441     $self->event_update_games;
442 pcg 1.4 }
443    
444     sub event_join {
445     my ($self) = @_;
446     $self->SUPER::event_join;
447    
448     $::config->{rooms}{$self->{channel}} = 1;
449 pcg 1.1 }
450    
451     sub event_update_roominfo {
452     my ($self) = @_;
453    
454     $self->{text}->insert(undef, undef, undef, "$self->{owner}: $self->{description}\n\n");
455     }
456    
457     sub inject_msg_room {
458     my ($self, $msg) = @_;
459     return unless $self->{channel} == $msg->{channel};
460    
461     $self->{text}->insert(undef, undef, undef, "$msg->{name}: $msg->{message}\n");
462     }
463    
464     #############################################################################
465    
466     package game;
467    
468     use base KGS::Listener::Game;
469     use base KGS::Game;
470    
471     sub new {
472     my $self = shift;
473     $self = $self->SUPER::new(@_);
474    
475     $self->listen($self->{conn});
476    
477     $self->{window} = new Gtk::Window 'toplevel';
478     $self->{window}->set_title("KGS Game ".$self->user0." ".$self->user1);
479     ::state $self->{window}, "game::window", undef, window_size => [600, 500];
480    
481     $self->{window}->signal_connect(delete_event => sub { $self->part });
482    
483     $self->{window}->add(my $hpane = new Gtk::HPaned);
484     ::state $hpane, "game::hpane", undef, hpane_position => 500;
485    
486 elmex 1.6 $hpane->add(my $canvas = new Gnome::Canvas);
487     $self->{canvas} = $canvas;
488     # $canvas->set_pixels_per_unit(300);
489     $canvas->set_scroll_region(0,0,400,400);
490     $self->{board} = [[]];
491     # $canvas->style->bg('normal', $canvas->style->white);
492     {
493     my $croot = $canvas->root;
494     my $cgroup = $croot->new($croot, "Gnome::CanvasGroup");
495     my $w = 400;
496     my $pad = 26;
497     my $box = $cgroup->new($cgroup,"Gnome::CanvasRect",
498     x1 => 0, x2 => $w,
499     y1 => 0, y2 => $w,
500     outline_color => "green",
501     fill_color => "green",
502     width_pixels => 2,
503     );
504     my $box2 = $cgroup->new($cgroup,"Gnome::CanvasRect",
505     x1 => $pad, x2 => $w - $pad,
506     y1 => $pad, y2 => $w - $pad,
507     outline_color => "brown",
508     fill_color => "brown",
509     width_pixels => 2,
510     );
511     my @lines;
512     my $x1 = $pad * 2; # == my $y1 = 30;
513     my $x2 = $w - ($pad * 2); # == my $y2 = 270;
514     my $w = $x2 - $x1;
515     my $s = $self->{size} - 1;
516    
517     for (my $i = 0; $i <= $s; $i++) { # one more iteration for the finishing lines
518     push @lines,
519     $cgroup->new($cgroup,"Gnome::CanvasLine",
520     points => [ $x1, $x1 + ($w/$s)*$i, $x1 + $w, $x1 + ($w/$s)*$i ],
521     fill_color => "darkbrown",
522     width_pixels => 1);
523     push @lines,
524     $cgroup->new($cgroup,"Gnome::CanvasLine",
525     points => [ $x1 + ($w/$s)*$i, $x1, $x1 + ($w/$s)*$i, $x1 + $w ],
526     fill_color => "darkbrown",
527     width_pixels => 1);
528     }
529     $self->{board_gfx}->{lines} = \@lines;
530     my $stones = [[]];
531     my $wf = ($w/$s);
532     for (my $x = 0; $x < $self->{size}; $x++) {
533     for (my $y = 0; $y < $self->{size}; $y++) {
534     $stones->[$x]->[$y]->[0] = # black stone
535     $cgroup->new($cgroup,"Gnome::CanvasEllipse",
536     x1 => $x1 + $wf*$x - ($wf/2.2), x2 => $x1 + $wf*$x + ($wf/2),
537     y1 => $x1 + $wf*$y - ($wf/2.2), y2 => $x1 + $wf*$y + ($wf/2),
538     fill_color => "black");
539     $stones->[$x]->[$y]->[0]->hide;
540     $stones->[$x]->[$y]->[1] = # black stone
541     $cgroup->new($cgroup,"Gnome::CanvasEllipse",
542     x1 => $x1 + $wf*$x - ($wf/2.2), x2 => $x1 + $wf*$x + ($wf/2),
543     y1 => $x1 + $wf*$y - ($wf/2.2), y2 => $x1 + $wf*$y + ($wf/2),
544     fill_color => "white");
545     $stones->[$x]->[$y]->[1]->hide;
546     }
547     }
548     $self->{board_gfx}->{stones} = $stones;
549    
550     # smooth => 1,
551     # spline_steps => 50
552     # $line->hide;
553     }
554     $canvas->show();
555 pcg 1.1
556    
557     $hpane->add(my $vpane = new Gtk::VPaned);
558     ::state $vpane, "game", $self->{name}, vpane_position => 80;
559    
560     $vpane->add(my $sw = new Gtk::ScrolledWindow);
561     $sw->set_policy("automatic", "always");
562    
563     $sw->add($self->{userlist} = new_with_titles Gtk::CList "User", "Rank", "Flags");
564     ::clist_autosort $self->{userlist};
565     ::state $self->{userlist}, "room::userlist", $self->{name}, clist_column_widths => [120, 30];
566    
567     $vpane->add(my $vbox = new Gtk::VBox);
568    
569     $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
570     $sw->set_policy("automatic", "always");
571    
572     $sw->add($self->{text} = new Gtk::Text);
573    
574     $vbox->pack_start(($self->{entry} = new Gtk::Entry), 0, 1, 0);
575     $self->{entry}->signal_connect(activate => sub {
576     my $text = $self->{entry}->get_text;
577     # add message
578     $self->{entry}->set_text("");
579     });
580    
581     $self;
582     }
583    
584     sub event_update {
585     my ($self) = @_;
586    
587     $self->{event_update} ||= Gtk->timeout_add(200, sub {
588     my $l = $self->{userlist};
589    
590     $l->freeze;
591     my $pos = $l->get_vadjustment->get_value;
592     $l->clear;
593    
594     my $row = 0;
595     for (values %{$self->{users}}) {
596     $l->append($_->{name});
597     $l->set_row_data($row++, $_);
598     }
599     $l->sort;
600     $l->get_vadjustment->set_value($pos);
601     $l->thaw;
602    
603     delete $self->{event_update};
604     0;
605     });
606     }
607    
608     sub join {
609     my ($self) = @_;
610     $self->SUPER::join;
611    
612     $self->{window}->show_all;
613     }
614    
615     sub part {
616     my ($self) = @_;
617     $self->SUPER::part;
618    
619     $self->{window}->hide_all;
620     $self->event_update;
621     }
622    
623     sub event_update_tree {
624     my ($self) = @_;
625    
626 elmex 1.6 # if (not defined $self->{board_pm}) {
627     # $self->{board_pm} = new Gtk::Gdk::Pixmap ($self->{board}->window, 100, 100, -1);
628     # $self->{board_pm}->draw_rectangle($self->{board}->style->white_gc, 1, 0, 0, 100, 100);
629     # }
630     # my $red = $self->{board}->window->get_colormap->color_alloc( { red => 65000, green => 0, blue => 0 } );
631     # my $red_gc = new Gtk::Gdk::GC ( $self->{board}->window );
632     # $red_gc->set_foreground( $red );
633     #
634     # my $px = $self->{board_pm};
635    
636 pcg 1.1 for my $x (0 .. $self->{size} - 1) {
637     for my $y (0 .. $self->{size} - 1) {
638 elmex 1.6 # $self->{board}[$x][$y] = 0;
639     $self->{board_gfx}->{stones}->[$x]->[$y]->[0]->hide;
640     $self->{board_gfx}->{stones}->[$x]->[$y]->[1]->hide;
641 pcg 1.1 }
642     }
643    
644     for (0..$self->{node}) {
645     while (my ($k, $v) = each %{$self->{tree}[$_]}) {
646     if ($k eq "move7") {
647     if ($v->[1] < 255) {
648 elmex 1.6 # $self->{board_gfx}->{stones}[$x][$y]->[0]->show;
649     ($v->[0] == 0) &&
650     $self->{board_gfx}->{stones}->[$v->[1]]->[$v->[2]]->[0]->show;
651     ($v->[0] == 1) &&
652     $self->{board_gfx}->{stones}->[$v->[1]]->[$v->[2]]->[1]->show;
653     # $self->{board}[$v->[1]][$v->[2]] = ($v->[0] + 1); # ("X", "O", "-")
654     # 0 1 2 (+1)
655 pcg 1.1 }
656     }
657     }
658     }
659     }
660 pcg 1.5
661     package KGS::Game::Board;
662     package KGS::Game::Node;
663     package KGS::Game::Tree;
664 pcg 1.1
665     1;
666    
667    
668