ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/gde/GCE/MainWindow.pm
Revision: 1.86
Committed: Wed Apr 21 10:22:20 2010 UTC (14 years, 1 month ago) by elmex
Branch: MAIN
CVS Tags: HEAD
Changes since 1.85: +5 -0 lines
Log Message:
eval tool can save scripts now.

File Contents

# Content
1 package GCE::MainWindow;
2
3 =head1 NAME
4
5 GCE::MainWindow - the main window class for gde
6
7 =cut
8
9 use Cwd qw/abs_path getcwd/;
10 use Gtk2;
11 use Gtk2::Gdk::Keysyms;
12 use Gtk2::SimpleMenu;
13
14 use Deliantra;
15 use Deliantra::Map;
16 use Deliantra::MapWidget;
17
18 use GCE::AttrEdit;
19 use GCE::MapEditor;
20 use GCE::StackView;
21 use GCE::EditAction;
22 use GCE::PickWindow;
23
24 use Glib::Object::Subclass
25 Gtk2::Window;
26
27 use GCE::Util;
28 use GCE::DragHelper;
29
30 use strict;
31
32 my $recentfile = "$Deliantra::VARDIR/gderecent";
33
34 # XXX: make a recursive call from save_layout to all (interesting) sub-widgets
35 sub save_layout {
36 my ($self) = @_;
37
38 # $main::CFG->{attr_edit_on} = exists $self->{attr_edit} ? 1 : 0;
39 $main::CFG->{stack_view_on} = exists $self->{sv} ? 1 : 0;
40 $main::CFG->{picker_on} = exists $self->{last_pick_window} ? 1 : 0;
41 $main::CFG->{main_window} = main::get_pos_and_size ($self);
42 $main::CFG->{stack_view} = main::get_pos_and_size ($self->{sv_win}) if $self->{sv_win};
43 $main::CFG->{attr_view} = main::get_pos_and_size ($self->{attr_edit_win}) if $self->{attr_edit_win};
44
45 if ($self->{last_map_window}) {
46 $main::CFG->{map_window} = main::get_pos_and_size ($self->{last_map_window});
47 $self->{last_map_window}->save_layout ();
48 }
49
50 $self->{worldmap_coord_query}->save_layout ()
51 if $self->{worldmap_coord_query};
52
53 $main::CFG->{last_folders} = $self->{fc_last_folders};
54
55 $main::CFG->{open_pickers} = [];
56
57 for (@{$self->{open_pick_windows}}) {
58
59 next unless defined $_;
60
61 push @{$main::CFG->{open_pickers}}, {
62 p_and_s => main::get_pos_and_size ($_),
63 selection => $_->{last_selection}
64 };
65 }
66
67 $self->{attr_edit}->save_layout;
68
69 $self->write_cfg;
70 }
71
72 sub write_cfg {
73 my ($self) = @_;
74 main::write_cfg ("$Deliantra::VARDIR/gdeconfig");
75 }
76
77 sub load_layout {
78 my ($self) = @_;
79
80 $self->{fc_last_folders} = $main::CFG->{last_folders};
81
82 # $main::CFG->{attr_edit_on}
83 # and $self->show_attr_editor;
84
85 $main::CFG->{stack_view_on}
86 and $self->show_stack_view;
87
88 for (@{$main::CFG->{open_pickers}}) {
89 $self->open_pick_window ($_);
90 }
91
92 $self->{attr_edit}->load_layout;
93 }
94
95 sub open_map_editor {
96 my ($self, $mapfile) = @_;
97
98 my $mapkey;
99 unless (ref $mapfile) {
100 # unless (File::Spec->file_name_is_absolute ($mapfile)) {
101 # $mapfile = File::Spec->rel2abs ($mapfile);
102 # }
103 $mapkey = abs_path ($mapfile);
104 # File::Spec->abs2rel ($mapfile, File::Spec->catfile ($::MAPDIR));
105 } else {
106 $mapkey = "$mapfile";
107 }
108
109 # XXX: last_map_window is a dirty trick to get the position and size
110 # for save layout
111
112 if (defined $self->{loaded_maps}->{$mapkey}) {
113 $self->{loaded_maps}->{$mapkey}->get_toplevel->present;
114 return;
115 }
116
117 my $w = $self->{last_map_window} = GCE::MapEditor->new;
118
119 $self->{editors}->{$w} = $w;
120
121 $w->signal_connect (destroy => sub {
122 my ($w) = @_;
123 $w->close_windows;
124 delete $self->{loaded_maps}->{$w->{mapkey}};
125 delete $self->{editors}->{$w};
126 0;
127 });
128
129 $self->{loaded_maps}->{$mapkey} = $w;
130
131 eval { $w->open_map ($mapfile, $mapkey) };
132 if ($@) {
133 quick_msg ($self, "$@", 1);
134 $w->close_windows;
135 delete $self->{loaded_maps}->{$w->{mapkey}};
136 delete $self->{editors}->{$w};
137 $w->destroy;
138 return;
139 }
140
141 $w->set_edit_tool ($self->{sel_editaction});
142
143 $w->show_all;
144 }
145
146 sub show_help_window {
147 my ($self) = @_;
148
149 return if defined $self->{help_win};
150 require Gtk2::Ex::PodViewer;
151 my $w = $self->{help_win} = Gtk2::Window->new;
152 $w->set_title ("deliantra editor - help");
153 $w->set_default_size (500, 300);
154 $w->signal_connect (destroy => sub {
155 $self->{help_win}->hide; $self->{help_win} = undef;
156 0
157 });
158 $w->add (my $sw = Gtk2::ScrolledWindow->new);
159 $sw->add (my $h = Gtk2::Ex::PodViewer->new);
160 $h->load_string ($::DOCUMENTATION);
161 $w->show_all;
162 }
163
164 sub show_stack_view {
165 my ($self) = @_;
166
167 return if defined $self->{sv};
168
169 my $w = $self->{sv_win} = Gtk2::Window->new ('toplevel');
170 $w->set_title ('deliantra editor - stack view');
171 $w->signal_connect (destroy => sub { delete $self->{sv}; 0 });
172 $w->add ($self->{sv} = GCE::StackView->new);
173
174 main::set_pos_and_size ($w, $main::CFG->{stack_view}, 150, 250);
175
176 $w->show_all;
177 }
178
179 sub show_editor_properties {
180 my ($self) = @_;
181
182 return if $self->{prop_edit};
183
184 my $w = $self->{prop_edit} = Gtk2::Window->new;
185 $w->set_title ("deliantra editor - preferences");
186 $w->add (my $t = Gtk2::Table->new (2, 5));
187 $t->attach_defaults (my $lbl1 = Gtk2::Label->new ("LIBDIR"), 0, 1, 0, 1);
188 $t->attach_defaults (my $lib = Gtk2::Entry->new, 1, 2, 0, 1);
189 $lib->set_text ($::CFG->{LIBDIR});
190 $t->attach_defaults (my $lbl2 = Gtk2::Label->new ("MAPDIR"), 0, 1, 1, 2);
191 $t->attach_defaults (my $map = Gtk2::Entry->new, 1, 2, 1, 2);
192 $map->set_text ($::CFG->{MAPDIR});
193 $t->attach_defaults (my $lbl1 = Gtk2::Label->new ("Username"), 0, 1, 2, 3);
194 $t->attach_defaults (my $usern = Gtk2::Entry->new, 1, 2, 2, 3);
195 $usern->set_text ($::CFG->{username});
196 $t->attach_defaults (my $save = Gtk2::Button->new ('save'), 0, 2, 3, 4);
197 $save->signal_connect (clicked => sub {
198 $::CFG->{LIBDIR} = $lib->get_text;
199 $::CFG->{MAPDIR} = $map->get_text;
200 $::LIBDIR = $::CFG->{LIBDIR} if $::CFG->{LIBDIR};
201 $::MAPDIR = $::CFG->{MAPDIR} if $::CFG->{MAPDIR};
202 $::CFG->{username} = $usern->get_text;
203 Deliantra::set_libdir ($::LIBDIR);
204 Deliantra::load_archetypes;
205 Deliantra::load_tilecache;
206 main::write_cfg ("$Deliantra::VARDIR/gdeconfig");
207 $w->destroy;
208 });
209 $t->attach_defaults (my $close = Gtk2::Button->new ('close'), 0, 2, 4, 5);
210 $close->signal_connect (clicked => sub { $w->destroy });
211
212 $w->signal_connect (destroy => sub { delete $self->{prop_edit}; 0 });
213
214 main::set_pos_and_size ($w, $main::CFG->{prop_edit}, 200, 200);
215
216 $w->show_all;
217 }
218
219 sub show_attr_editor {
220 my ($self) = @_;
221
222 return if $self->{attr_edit_win};
223
224 my $w = $self->{attr_edit_win} = Gtk2::Window->new;
225 $w->set_title ("deliantra editor - edit attrs");
226 $w->add ($self->{attr_edit} = GCE::AttrEdit->new);
227 $w->signal_connect (destroy => sub { delete $self->{attr_edit_win}; 0 });
228
229 main::set_pos_and_size ($w, $main::CFG->{attr_view}, 400, 300, 250, 0);
230
231 $w->show_all;
232 }
233
234 sub update_attr_editor {
235 my ($self, $ar, $map, $x, $y) = @_;
236
237 if (ref ($ar) ne 'GCE::ArchRef') { require Carp; Carp::confess ("$ar no ARCHREF!") }
238
239 $self->{attr_edit}
240 or return;
241
242 $self->{attr_edit}->set_arch ($ar, 1);
243 $self->{attr_edit_win}->set_title ("deliantra editor - edit " . $ar->longname);
244 }
245
246 sub update_map_pos {
247 my ($self, $mapedit, $x, $y) = @_;
248 $self->{sv}->maybe_update_stack_for ($mapedit, $x, $y)
249 if $self->{sv};
250 }
251
252 sub update_stack_view {
253 my ($self, $mapedit, $x, $y) = @_;
254
255 return unless $self->{sv};
256
257 $self->{sv}->set_stack ($mapedit, $x, $y);
258 }
259
260 sub open_pick_window {
261 my ($self, $layout) = @_;
262
263 # XXX: Yes, also fix this, save _every_ pick window and their positions and their
264 # selection
265 my $p = GCE::PickWindow->new ();
266
267 push @{$self->{open_pick_windows}}, $p;
268
269 my $idx = (@{$self->{open_pick_windows}}) - 1;
270
271 $p->signal_connect ('delete-event' => sub {
272 $self->{open_pick_windows}->[$idx] = undef;
273 });
274
275 if ($layout) {
276 main::set_pos_and_size ($p, $layout->{p_and_s}, 200, 200);
277 }
278
279 $p->show_all;
280
281 $p->set_selection ($layout->{selection});
282 }
283
284 sub add_recent {
285 my ($self, $entry) = @_;
286 our @recent_entries;
287
288 @recent_entries = grep { $_ ne $entry } @recent_entries;
289 unshift @recent_entries, $entry;
290 splice @recent_entries, 5;
291
292 open my $fh, ">$recentfile" or die "Can't write to file $recentfile: $!";
293 binmode $fh;
294 local $/;
295 print $fh (join "\0", @recent_entries);
296 close $fh;
297
298 $self->build_menu;
299 }
300
301 sub escape_filename {
302 my $str = shift;
303
304 $str = Glib::filename_to_unicode($str);
305
306 # escape to prevent Gtk2::SimpleMenu parsing these especially
307 $str =~ s/\//\\\//g;
308 $str =~ s/_/-/g;
309
310 $str;
311 }
312
313 sub recent {
314 my ($self) = @_;
315 my @recent;
316
317 our @recent_entries;
318
319 foreach my $entry (@recent_entries) {
320 push @recent, escape_filename($entry) => {
321 callback => sub {
322 $self->open_map_editor($entry);
323 $self->build_menu
324 }
325 };
326 }
327
328 push @recent, Empty => { callback => sub {} }
329 unless @recent;
330
331 @recent;
332 }
333
334 sub build_menu {
335 my ($self) = @_;
336
337 my $menu_tree = [
338 _File => {
339 item_type => '<Branch>',
340 children => [
341 _New => {
342 callback => sub { $self->new_cb },
343 accelerator => '<ctrl>N'
344 },
345 _Open => {
346 callback => sub { $self->open_cb },
347 accelerator => '<ctrl>O'
348 },
349 'Open special' => {
350 item_type => '<Branch>',
351 children => [
352 "world map at"=> {
353 callback => sub { $self->open_worldmap_cb },
354 },
355 "recent files"=> {
356 item_type => '<Branch>',
357 children => [ $self->recent ],
358 },
359 ]
360 },
361 "_Save Layout" => {
362 callback => sub { $self->save_layout },
363 accelerator => '<ctrl>L'
364 },
365 "_Preferences" => {
366 callback => sub { $self->show_editor_properties },
367 accelerator => "<ctrl>T"
368 },
369 _Quit => {
370 callback => sub { Gtk2->main_quit },
371 accelerator => '<ctrl>Q'
372 },
373 ]
374 },
375 _Dialogs => {
376 item_type => '<Branch>',
377 children => [
378 "_Picker" => {
379 callback => sub { $self->open_pick_window },
380 accelerator => "<ctrl>P"
381 },
382 "_Stack View" => {
383 callback => sub { $self->show_stack_view },
384 accelerator => "<ctrl>V"
385 },
386 "_Attributes" => {
387 callback => sub { $self->show_attr_editor },
388 accelerator => "<ctrl>A"
389 },
390 ]
391 },
392 _Help => {
393 item_type => '<Branch>',
394 children => [
395 _Manual => {
396 callback => sub { $self->show_help_window },
397 accelerator => "<ctrl>H"
398 },
399 ]
400 },
401 ];
402
403 my $men =
404 Gtk2::SimpleMenu->new (
405 menu_tree => $menu_tree,
406 default_callback => \&default_cb,
407 );
408
409 for ($self->{vb}->get_children) { # Rebuild menu
410 if ($_->isa ('Gtk2::MenuBar')) {
411 $_->hide;
412 $self->{vb}->remove($_);
413
414 $self->{vb}->pack_start ($men->{widget}, 0, 1, 0);
415 $self->{vb}->reorder_child ($men->{widget}, 0);
416 $self->{vb}->show_all;
417 }
418 }
419
420 $self->add_accel_group ($men->{accel_group});
421
422 return $men->{widget};
423 }
424
425 sub add_button {
426 my ($self, $table, $plcinfo, $lbl, $cb) = @_;
427
428 my ($lx, $ly) = @{$plcinfo->{next}};
429
430 unless ($lx < $plcinfo->{width}) {
431
432 $ly++;
433 $lx = 0;
434 }
435
436 $ly < $plcinfo->{height}
437 or die "too many buttons, make table bigger!";
438
439 $table->attach_defaults (my $btn = Gtk2::Button->new_with_mnemonic ($lbl), $lx, $lx + 1, $ly, $ly + 1);
440 $btn->signal_connect (clicked => $cb);
441
442 $plcinfo->{next} = [$lx + 1, $ly];
443 }
444
445 sub build_buttons {
446 my ($self) = @_;
447
448 my $tbl = Gtk2::Table->new (2, 4);
449 my $plcinfo = { width => 2, height => 4, next => [0, 0] };
450
451 $self->{edit_collection}{pick} = GCE::EditAction::Pick->new;
452 $self->{edit_collection}{place} = GCE::EditAction::Place->new;
453 $self->{edit_collection}{erase} = GCE::EditAction::Erase->new;
454 $self->{edit_collection}{select} = GCE::EditAction::Select->new;
455 $self->{edit_collection}{perl} = GCE::EditAction::Perl->new;
456 $self->{edit_collection}{connect} = GCE::EditAction::Connect->new;
457 $self->{edit_collection}{followexit} = GCE::EditAction::FollowExit->new;
458
459 $self->set_edit_tool ('pick');
460
461 $self->add_button ($tbl, $plcinfo, "P_ick", sub { $self->set_edit_tool ('pick') });
462 $self->add_button ($tbl, $plcinfo, "_Place", sub { $self->set_edit_tool ('place') });
463 $self->add_button ($tbl, $plcinfo, "_Erase", sub { $self->set_edit_tool ('erase') });
464 $self->add_button ($tbl, $plcinfo, "_Select", sub { $self->set_edit_tool ('select') });
465 $self->add_button ($tbl, $plcinfo, "Eva_l", sub { $self->set_edit_tool ('perl') });
466 $self->add_button ($tbl, $plcinfo, "Connec_t", sub { $self->set_edit_tool ('connect') });
467 $self->add_button ($tbl, $plcinfo, "_Follow Exit", sub { $self->set_edit_tool ('followexit') });
468
469 return $tbl;
470 }
471
472 sub set_edit_tool {
473 my ($self, $name) = @_;
474
475 if ($name eq 'pick') {
476 $self->update_edit_tool ($self->{edit_collection}{pick}, "Pick");;
477 } elsif ($name eq 'place') {
478 $self->update_edit_tool ($self->{edit_collection}{place}, "Place");;
479 } elsif ($name eq 'erase') {
480 $self->update_edit_tool ($self->{edit_collection}{erase}, "Erase");;
481 } elsif ($name eq 'select') {
482 $self->update_edit_tool ($self->{edit_collection}{select}, "Select");;
483 $self->{edit_collection}{select}->update_overlay;
484 } elsif ($name eq 'perl') {
485 $self->update_edit_tool ($self->{edit_collection}{perl}, "Eval");;
486 } elsif ($name eq 'connect') {
487 $self->update_edit_tool ($self->{edit_collection}{connect}, "Connect");;
488 } elsif ($name eq 'followexit') {
489 $self->update_edit_tool ($self->{edit_collection}{followexit}, "Follow Exit");;
490 }
491 }
492
493 sub update_edit_tool {
494 my ($self, $tool, $name) = @_;
495
496 for (values %{$self->{loaded_maps}}) {
497 $_->{map}->overlay ('selection')
498 }
499
500 $self->{edit_tool}->set_text ($name);
501 $self->{sel_editaction} = $tool;
502
503 my $widget = $tool->tool_widget;
504
505 for ($self->{edit_tool_cont}->get_children) {
506 $_->hide;
507 $self->{edit_tool_cont}->remove ($_);
508 }
509
510 $_->set_edit_tool ($self->{sel_editaction}) for (values %{$self->{editors}});
511
512 defined $widget or return;
513
514 $self->{edit_tool_cont}->add ($widget);
515 $widget->show_all;
516 }
517
518 sub update_pick_view {
519 my ($self, $arch) = @_;
520
521 defined $arch->{_face}
522 or $arch = $Deliantra::ARCH{$arch->{_name}};
523
524 fill_pb_from_arch ($self->{pick_view_pb}, $arch);
525 $self->{pick_view_img}->set_from_pixbuf ($self->{pick_view_pb});
526
527 $self->{pick_view_btn}->set_label ($arch->{_name});
528 }
529
530 sub INIT_INSTANCE {
531 my ($self) = @_;
532
533 {
534 open my $fh, "<$recentfile";
535 binmode $fh;
536 local $/;
537 our @recent_entries = split /\0/, <$fh>;
538 close $fh;
539 }
540
541 $::MAINWIN = $self;
542
543 $self->set_title ("deliantra editor - toolbox");
544
545 $self->{edit_tool} = Gtk2::Label->new;
546 $self->{edit_tool_cont} = Gtk2::VBox->new;
547
548 $self->add (my $vb = $self->{vb} = Gtk2::VBox->new);
549 $vb->pack_start ($self->build_menu, 0, 1, 0);
550 $vb->pack_start (my $tbl = $self->build_buttons, 0, 1, 0);
551
552 $vb->pack_start (Gtk2::HSeparator->new, 0, 1, 0);
553 $vb->pack_start ($self->{edit_tool}, 0, 1, 0);
554
555 $vb->pack_start (Gtk2::HSeparator->new, 0, 1, 0);
556 $vb->pack_start ($self->{edit_tool_cont}, 1, 1, 0);
557
558 # XXX:load $ARGV _cleanly_?
559 $self->open_map_editor ($_)
560 for @ARGV;
561
562 $self->signal_connect ('delete-event' => sub {
563 Gtk2->main_quit;
564 });
565
566 ::set_pos_and_size ($self, $main::CFG->{main_window}, 150, 200, 0, 0);
567
568
569 $self->show_attr_editor;
570 }
571
572 sub new_cb {
573 my ($self) = @_;
574
575 my $w = Gtk2::Window->new ('toplevel');
576 my $width = [width => 20];
577 my $height = [height => 20];
578 $w->add (my $tbl = Gtk2::Table->new (2, 3));
579 add_table_widget ($tbl, 0, $width, 'string');
580 add_table_widget ($tbl, 1, $height, 'string');
581 add_table_widget ($tbl, 2, 'new', 'button', sub {
582 if ($width->[1] > 0 and $height->[1] > 0) {
583 my $map = Deliantra::Map->new ($width->[1], $height->[1]);
584 $map->{info}->{width} = $width->[1];
585 $map->{info}->{height} = $height->[1];
586 $map->resize ($width->[1], $height->[1]);
587 $self->open_map_editor ($map);
588 }
589 $w->destroy;
590 1;
591 });
592 add_table_widget ($tbl, 3, 'close', 'button', sub { $w->destroy });
593 $w->show_all;
594 }
595
596 sub new_filechooser {
597 my ($self, $title, $save, $filename) = @_;
598
599 $title ||= 'deliantra editor - open map';
600 my $fc = new Gtk2::FileChooserDialog (
601 $title, undef, $save ? 'save' : 'open', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok'
602 );
603
604 my @shortcut_folders =
605 grep { $_ && ($_ ne '') && -e $_ } keys %{$self->{fc_last_folders}};
606
607 $fc->add_shortcut_folder ($_) for @shortcut_folders;
608
609 unless (grep { $::MAPDIR eq $_ } @shortcut_folders) {
610 $fc->add_shortcut_folder ($::MAPDIR)
611 if -d $::MAPDIR;
612 }
613
614 $fc->set_current_folder (getcwd);
615
616 if ($filename) {
617 $fc->set_filename ($filename);
618 }
619
620 $fc
621 }
622
623 sub new_coord_query {
624 my ($self, $finishcb) = @_;
625
626 my $coordhash = { x => 105, y => 115, worldmap => 1, overlay => 0 };
627 my $diag = GCE::HashDialogue->new;
628 $self->{"worldmap_coord_query"} = $diag;
629 $diag->signal_connect (destroy => sub {
630 delete $self->{"worldmap_coord_query"};
631 });
632 $diag->init (
633 layout_name => 'worldmap_coord_query',
634 info => "Open worldmap at ...",
635 dialog_default_size => [ 200, 200, 200, 0 ],
636 title => 'Worldmap coordinate entry',
637 ref_hash => $coordhash,
638 dialog => [
639 [x => 'X Coordinate' => 'spin', sub { (0, 999, 1) }],
640 [y => 'Y Coordinate' => 'spin', sub { (0, 999, 1) }],
641 [worldmap => 'Open worldmap' => 'check'],
642 [overlay => 'Open overlay (CF+)' => 'check'],
643 ],
644 save_button_label => 'open',
645 save_cb => sub {
646 $finishcb->($_[0]);
647 }
648 );
649 $diag->show_all;
650 }
651
652 sub open_worldmap_cb {
653 my ($self) = @_;
654
655 $self->new_coord_query (sub {
656 my ($info) = @_;
657 my ($x, $y) = ($info->{x}, $info->{y});
658 my ($worldmap, $overlay) = ($info->{worldmap}, $info->{overlay});
659 $self->open_map_editor ($::MAPDIR . "/world/world_$x\_$y")
660 if $worldmap;
661 $self->open_map_editor ($::MAPDIR . "/world-overlay/world_$x\_$y")
662 if $overlay;
663 });
664 }
665
666 sub open_cb {
667 my ($self) = @_;
668
669 my $fc = $self->new_filechooser;
670
671 if ('ok' eq $fc->run) {
672
673 $self->{fc_last_folder} = $fc->get_current_folder;
674 $self->{fc_last_folders}->{$self->{fc_last_folder}}++;
675
676 $self->open_map_editor ($fc->get_filename);
677 }
678
679 $fc->destroy;
680 }
681
682 sub get_pick {
683 my ($self) = @_;
684
685 $self->{attr_edit}
686 or die "Couldn't find attribute editor! SERIOUS BUG!";
687
688 # XXX: This is just to make sure that this function always returns something
689
690 my $ar = $self->{attr_edit}->get_arch;
691 return { _name => 'platinacoin' } unless defined $ar;
692 return $ar->getarch || { _name => 'platinacoin' };
693 }
694
695 =head1 AUTHOR
696
697 Marc Lehmann <schmorp@schmorp.de>
698 http://home.schmorp.de/
699
700 Robin Redeker <elmex@ta-sa.org>
701 http://www.ta-sa.org/
702
703 =cut
704 1;
705