ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/kgsueme/bin/kgsueme-gtk1
Revision: 1.3
Committed: Tue Jun 1 20:45:36 2004 UTC (19 years, 11 months ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.2: +0 -0 lines
State: FILE REMOVED
Log Message:
*** empty log message ***

File Contents

# Content
1 #!/usr/bin/perl
2
3 # this is only of historic interest
4
5 use Gtk;
6 use Gtk::Gdk;
7 use Gtk::Gdk::Pixbuf;
8 #use Gtk::Gdk::ImlibImage;
9
10 use KGS::Protocol;
11 use KGS::Listener::Debug;
12
13 use Audio::Data;
14 use Audio::Play;
15
16 use IO::Socket::INET;
17 use List::Util;
18 use Errno;
19
20 init Gtk;
21
22 our $HACK = 1; # do NEVER enable. ;)
23 our $DEBUG_EXPOSE = 0;
24
25 if ($HACK) {
26 $KGS::debug = 1;
27 }
28
29 our $VERSION = "0.1";
30
31 our $config;
32 our $LIBDIR = ".";
33 our $IMGDIR = "$LIBDIR/images";
34 our $SNDDIR = "$LIBDIR/sounds";
35
36 sub load_img {
37 new_from_file Gtk::Gdk::Pixbuf "$IMGDIR/$_[0]"
38 # load_image Gtk::Gdk::ImlibImage "$IMGDIR/$_[0]"
39 or die "$IMGDIR/$_[0]: $!";
40 }
41
42 my @fontchars = ('A' .. 'Z', 0 .. 9);
43
44 our @black_img = load_img "b-01.png";
45 our @white_img = map +(load_img "w-0$_.png"), 1,2,3,4,5;
46 our @triangle_img = map +(load_img "triangle-$_.png"), qw(b w);
47 our @square_img = map +(load_img "square-$_.png"), qw(b w);
48 our @circle_img = map +(load_img "circle-$_.png"), qw(b w);
49 our $board_img = load_img "woodgrain-01.jpg";
50
51 our @font = (
52 [map +(load_img "font/$_-black.png"), @fontchars],
53 [map +(load_img "font/$_-white.png"), @fontchars],
54 );
55 our %fontmap;
56 @fontmap{@fontchars} = (0..25 + 10);
57 @fontmap{'a' .. 'z'} = (0..25);
58
59 {
60 #my $audioserver = new Audio::Play(0);
61 my %sound;
62 $SIG{CHLD} = 'IGNORE';
63
64 for (qw(alarm warning move pass ring connect user_unknown)) {
65 local $/;
66 open my $snd, "<", "$SNDDIR/$_"
67 or die "$SNDDIR/$_: $!";
68 binmode $snd;
69
70 $sound{$_} = new Audio::Data;
71 $sound{$_}->Load($snd);
72 }
73
74 sub play_sound {
75 my ($annoyancy, $sound) = @_;
76 # annoyany 1 => important, annoyance 2 => useful, annoyancy 3 => not useful
77 if (fork == 0) {
78 if (my $audioserver = new Audio::Play(1)) {
79 $audioserver->play ($sound{$sound});
80 }
81 Gtk->_exit(0);
82 }
83 }
84 }
85
86 {
87 use Storable ();
88 use Scalar::Util ();
89
90 my $staterc = "$ENV{HOME}/.kgsueme";
91
92 my $state = -r $staterc ? Storable::retrieve($staterc) : {};
93 my @widgets;
94
95 $config = $state->{config} ||= {};
96
97 $config->{speed} = 1;#d# optimize for speed or memory?
98 $config->{conserve_memory} = 0; # try to conserve memory at the expense of speed
99 $config->{randomize} = 0; # randomize placement of stones
100
101 # grr... more gtk+ brokenness
102 my %get = (
103 hpane_position => sub { ($_[0]->children)[0]->allocation->[2] },
104 vpane_position => sub { ($_[0]->children)[0]->allocation->[3] },
105 window_size => sub { [ @{$_[0]->allocation}[2,3] ] },
106 #window_pos => sub { die KGS::Listener::Debug::dumpval [ $_[0]->get_root_origin ] },
107 clist_column_widths => sub {
108 $_[0]{column_widths};
109 },
110 );
111
112 my %set = (
113 hpane_position => sub { $_[0]->set_position($_[1]) },
114 vpane_position => sub { $_[0]->set_position($_[1]) },
115 window_size => sub { $_[0]->set_default_size(@{$_[1]}) },
116 #window_pos => sub { $_[0]->set_uposition(@{$_[1]}) if @{$_[1]} },
117 clist_column_widths => sub {
118 my ($w, $v) = @_;
119 $v->[$_] && $w->set_column_width($_, $v->[$_]) for 0..$#$v;
120 $w->{column_widths} = $v;
121 $w->signal_connect(resize_column => sub { $v->[$_[1]] = $_[2]; });
122 },
123 );
124
125 sub state {
126 my ($widget, $class, $instance, %attr) = @_;
127
128 while (my ($k, $v) = each %attr) {
129 my ($set, $get) = $k =~ /=/ ? split /=/, $k : ($k, $k);
130 $v = $state->{$class}{"*"}{$get} if exists $state->{$class}{"*"}{$get};
131 $v = $state->{$class}{$instance}{$get} if exists $state->{$class}{$instance}{$get};
132 $set{$get} ? $set{$get}->($widget, $v) : $widget->set($set => $v);
133 }
134
135 $widget = [$widget, $class, $instance, \%attr];
136 Scalar::Util::weaken $widget->[0];
137
138 @widgets = (grep $_->[0], @widgets, $widget);
139 }
140
141 sub save_state {
142 for (@widgets) {
143 if ($_->[0]) {
144 my ($widget, $class, $instance, $attr) = @$_;
145
146 $widget->realize;
147
148 while (my ($k, $v) = each %$attr) {
149 my ($set, $get) = $k =~ /=/ ? split /=/, $k : ($k, $k);
150 $v = $get{$get} ? $get{$get}->($widget) : $widget->get($get);
151
152 $state->{$class}{"*"}{$get} = $v;
153 $state->{$class}{$instance}{$get} = $v;
154 }
155 }
156 ::status("save_state", "layout saved");
157 }
158
159 Storable::nstore($state, $staterc);
160 }
161 }
162
163 # make a clist unselectable
164 sub clist_autosort {
165 my $w = shift;
166 my ($c, $o) = (-1);
167 for (0..$w->columns-1) {
168 $w->signal_connect(click_column => sub {
169 if ($_[1] != $c) {
170 $c = $_[1];
171 $o = 0;
172 } else {
173 $o = !$o;
174 }
175 $w->set_sort_column($c);
176 $w->set_sort_type($o ? "descending" : "ascending");
177 $w->sort;
178 });
179 }
180
181 }
182
183 {
184 my $main = new kgsueme;
185
186 my %context_id;
187
188 sub status {
189 my ($type, $text) = @_;
190
191 $main->{status}->pop($context_id{$type}) if $context_id{$type};
192 $main->{status}->push($context_id{$type} ||= $main->{status}->get_context_id($type), $text) if $text;
193 }
194 }
195
196 if (1) {
197 use KGS::Constants;
198
199 for (19) {
200 my $board = new game %{Storable::retrieve "testboard.storable"};
201
202 if (0) {
203 $board->{board} = new KGS::Game::Board;
204 $board->{board}{board}[0][0] = MARK_B;
205 $board->{board}{board}[1][1] = MARK_GRAY_B | MARK_SMALL_W;
206 $board->{board}{board}[2][2] = MARK_W | MARK_TRIANGLE;
207 $board->{board}{board}[1][2] = MARK_B | MARK_LABEL;
208 $board->{board}{label}[1][2] = "198";
209 $board->{board}{board}[0][2] = MARK_W | MARK_LABEL;
210 $board->{board}{label}[0][2] = "AWA";
211 }
212 $board->{window}->show_all;
213 }
214 }
215
216 main Gtk;
217
218 #############################################################################
219
220 package kgsueme;
221
222 use base KGS::Listener;
223
224 sub new {
225 my $self = shift;
226 $self = $self->SUPER::new(@_);
227
228 $self->{conn} = new KGS::Protocol;
229
230 KGS::Listener::Debug->new->listen($self->{conn}, "any"); #d# debug only :)
231
232 $self->listen($self->{conn}, "login");
233
234 $self->{roomlist} = new roomlist conn => $self->{conn};
235
236 $self->{window} = new Gtk::Window 'toplevel';
237 $self->{window}->set_title('kgsueme');
238 ::state $self->{window}, "main::window", undef, window_size => [400, 100];
239 $self->{window}->signal_connect(delete_event => sub { main_quit Gtk; 1 });
240
241 $self->{window}->add(my $vbox = new Gtk::VBox);
242
243 $vbox->pack_start(($buttonbox = new Gtk::HButtonBox), 0, 1, 0);
244 $buttonbox->set_spacing(0);
245
246 my $button = sub {
247 $buttonbox->add(my $button = new Gtk::Button $_[0]);
248 signal_connect $button clicked => $_[1];
249 };
250
251 $button->("Login", sub { $self->login; });
252 $button->("Roomlist", sub { $self->{roomlist}->show; });
253 $button->("Save Config & Layout", sub { ::save_state });
254 $button->("Quit", sub { main_quit Gtk });
255
256 $vbox->pack_start((my $hbox = new Gtk::HBox), 0, 1, 0);
257
258 $hbox->add(new Gtk::Label "Login");
259
260 $hbox->add($self->{login} = new_with_max_length Gtk::Entry 12);
261 $self->{login}->set_text($::config->{login});
262
263 if ($::HACK) {
264 $self->{login}->signal_connect(activate => sub {
265 $self->{conn}{name} = $self->{login}->get_text;
266 });
267 }
268
269 $hbox->add(new Gtk::Label "Password");
270 $hbox->add($self->{password} = new Gtk::Entry);
271 $self->{password}->set_visibility(0);
272
273 $vbox->pack_start(($self->{status} = new Gtk::Statusbar), 0, 1, 0);
274
275 $self->{window}->show_all;
276
277 $self;
278 }
279
280 sub login {
281 my ($self) = @_;
282
283 $self->{conn}->disconnect;
284
285 # initialize new socket and connection
286 my $sock = new IO::Socket::INET PeerHost => "kgs.kiseido.com", PeerPort => "2379"
287 or die;
288
289 $sock->blocking(1);
290 $self->{conn}->handshake($sock);
291 $sock->blocking(0);
292
293 my $input; $input = input_add Gtk::Gdk fileno $sock, "read", sub {
294 # this is dorked
295 my $buf;
296 if (0 >= sysread $sock, $buf, 16384
297 and !$!{EINTR} and !$!{EAGAIN}) {
298 input_remove Gtk::Gdk $input;
299 $self->event_disconnect;
300 }
301 $self->{conn}->feed_data($buf);
302 };
303
304 # now login
305 $self->{conn}->login("kgsueme $VERSION $^O", $self->{login}->get_text, $self->{password}->get_text);
306 }
307
308 sub inject_login {
309 my ($self, $msg) = @_;
310
311 ::status("login", "logged in as '$self->{conn}{name}' with status '$msg->{result}'");
312 $::config->{login} = $self->{conn}{name};
313
314 if ($msg->{success}) {
315 for (keys %{$::config->{rooms}}) {
316 $self->{roomlist}->join_room($_);
317 }
318 ::play_sound 3, "connect";
319 } elsif ($msg->{result} eq "user unknown") {
320 ::play_sound 2, "user_unknown";
321 } else {
322 ::play_sound 2, "warning";
323 }
324 }
325
326 sub event_disconnect { }
327
328 #############################################################################
329
330 package roomlist;
331
332 use base KGS::Listener::Roomlist;
333
334 sub new {
335 my $self = shift;
336 $self = $self->SUPER::new(@_);
337
338 $self->listen($self->{conn});
339
340 $self->{window} = new Gtk::Window 'toplevel';
341 $self->{window}->set_title('KGS Rooms');
342 ::state $self->{window}, "roomlist::window", undef, window_size => [400, 200];
343
344 $self->{window}->signal_connect(delete_event => sub { $self->{window}->hide; 1 });
345
346 $self->{window}->add(my $vbox = new Gtk::VBox);
347
348 $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
349 $sw->set_policy("automatic", "always");
350
351 $sw->add($self->{roomlist} = new_with_titles Gtk::CList "Group", "Room Name", "Users", "Games", "Flags", "Channel");
352 $self->{roomlist}->set_selection_mode('multiple');
353 ::clist_autosort $self->{roomlist};
354 ::state $self->{roomlist}, "roomlist::roomlist", undef, clist_column_widths => [20, 200];
355
356 $self->{roomlist}->signal_connect(select_row => sub {
357 my $room = $self->{roomlist}->get_row_data($_[1])
358 or return;
359 $self->{roomlist}->unselect_all;
360 $self->join_room($room->{channel});
361 });
362
363 $self;
364 }
365
366 sub join_room {
367 my ($self, $channel) = @_;
368
369 $self->{room}{$channel} ||= room->new(channel => $channel, conn => $self->{conn}, users => {});
370 $self->{room}{$channel}->join;
371 }
372
373 sub show {
374 my ($self, $msg) = @_;
375
376 $self->msg(list_rooms => group => $_) for 0..5; # fetch all room names (should not!)
377 $self->{window}->show_all;
378 }
379
380 sub event_update_rooms {
381 my ($self) = @_;
382
383 $self->{event_update} ||= Gtk->timeout_add(200, sub {
384 my $l = $self->{roomlist};
385
386 $l->freeze;
387 my $pos = $l->get_vadjustment->get_value;
388 $l->clear;
389
390 my $row = 0;
391 for (values %{$self->{rooms}}) {
392 $l->append($_->{group}, $_->{name}, $_->{users}, $_->{games}, $_->{flags}, $_->{channel});
393 $l->set_row_data($row++, $_);
394 }
395 $l->sort;
396 $l->get_vadjustment->set_value($pos);
397 $l->thaw;
398
399 delete $self->{event_update};
400 0;
401 });
402 }
403
404 #############################################################################
405
406 package room;
407
408 use base KGS::Listener::Room;
409
410 sub new {
411 my $self = shift;
412 $self = $self->SUPER::new(@_);
413
414 $self->listen($self->{conn}, qw(msg_room:));
415
416 $self->{window} = new Gtk::Window 'toplevel';
417 $self->{window}->set_title("KGS Room $self->{name}");
418 ::state $self->{window}, "room::window", $self->{name}, window_size => [600, 400];
419
420 $self->{window}->signal_connect(delete_event => sub { $self->part; 1 });
421
422 $self->{window}->add(my $hpane = new Gtk::HPaned);
423 ::state $hpane, "room::hpane", $self->{name}, hpane_position => 200;
424
425 $hpane->add(my $vpane = new Gtk::VPaned);
426 ::state $vpane, "room::vpane", $self->{name}, vpane_position => 200;
427
428 $vpane->add(my $sw = new Gtk::ScrolledWindow);
429 $sw->set_policy("automatic", "always");
430
431 $sw->add($self->{gamelist} = new_with_titles Gtk::CList "T", "Black", "White", "Rules", "Notes");
432 ::clist_autosort $self->{gamelist};
433 ::state $self->{gamelist}, "room::gamelist", $self->{name}, clist_column_widths => [20, 120, 120, 120];
434
435 $self->{gamelist}->signal_connect(select_row => sub {
436 my $game = $self->{gamelist}->get_row_data($_[1])
437 or return;
438 $self->{game}{$game->{channel}} ||= new game %$game, conn => $self->{conn}, room => $self;
439 $self->{game}{$game->{channel}}->join;
440 $self->{gamelist}->unselect_all;
441 });
442
443 $vpane->add(my $vbox = new Gtk::VBox);
444
445 $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
446 $sw->set_policy("automatic", "always");
447
448 $sw->add($self->{text} = new Gtk::Text);
449
450 $vbox->pack_start(($self->{entry} = new Gtk::Entry), 0, 1, 0);
451 $self->{entry}->signal_connect(activate => sub {
452 my $text = $self->{entry}->get_text;
453 $self->say($text) if $text =~ /\S/;
454 $self->{entry}->set_text("");
455 });
456
457 $hpane->add(my $sw = new Gtk::ScrolledWindow);
458 $sw->set_policy("automatic", "always");
459
460 $sw->add($self->{userlist} = new_with_titles Gtk::CList "User", "Rank", "Flags");
461 ::clist_autosort $self->{userlist};
462 ::state $self->{userlist}, "room::userlist", $self->{name}, clist_column_widths => [120, 30];
463
464 $self;
465 }
466
467 sub join {
468 my ($self) = @_;
469 $self->SUPER::join;
470
471 $self->{window}->show_all;
472 }
473
474 sub part {
475 my ($self) = @_;
476 $self->SUPER::part;
477
478 delete $::config->{rooms}{$self->{channel}};
479 $self->{window}->hide_all;
480 }
481
482 sub inject_msg_room {
483 my ($self, $msg) = @_;
484
485 $self->{text}->insert(undef, undef, undef, "\n$msg->{name}: $msg->{message}");
486 }
487
488 sub event_update_users {
489 my ($self) = @_;
490
491 Gtk->timeout_remove (delete $self->{update_users}) if $self->{update_users};
492 $self->{update_users} ||= Gtk->timeout_add(100, sub {
493 return unless $self->{joined};
494
495 my $l = $self->{userlist};
496
497 $l->freeze;
498 my $pos = $l->get_vadjustment->get_value;
499 $l->clear;
500
501 my $row = 0;
502 for (values %{$self->{users}}) {
503 $l->append($_->{name});
504 $l->set_row_data($row++, $_);
505 }
506 $l->sort;
507 $l->get_vadjustment->set_value($pos);
508 $l->thaw;
509
510 delete $self->{update_users};
511 });
512 }
513
514 sub event_update_games {
515 my ($self) = @_;
516
517 $self->{event_update_games} ||= Gtk->timeout_add(200, sub {
518 my $l = $self->{gamelist};
519
520 $l->freeze;
521 my $pos = $l->get_vadjustment->get_value;
522 $l->clear;
523
524 my $row = 0;
525 for (values %{$self->{games}}) {
526 $l->append($_->type, $_->user0, $_->user1, $_->rules, $_->notes);
527 $l->set_row_data($row++, $_);
528 }
529 $l->sort;
530 $l->get_vadjustment->set_value($pos);
531 $l->thaw;
532
533 delete $self->{event_update_games};
534 0;
535 });
536 }
537
538 sub event_join {
539 my ($self) = @_;
540 $self->SUPER::event_join;
541
542 $::config->{rooms}{$self->{channel}} = 1;
543 }
544
545 sub event_update_roominfo {
546 my ($self) = @_;
547
548 $self->{text}->insert(undef, undef, undef, "\n$self->{owner}: $self->{description}\n");
549 }
550
551 #############################################################################
552
553 package game;
554
555 use KGS::Constants;
556 use KGS::Game::Board;
557
558 use base KGS::Listener::Game;
559 use base KGS::Game;
560
561 sub new {
562 my $self = shift;
563 $self = $self->SUPER::new(@_);
564
565 $self->listen($self->{conn});
566
567 $self->{window} = new Gtk::Window 'toplevel';
568 my $title = $self->{channel} ? $self->user0." ".$self->user1 : "Game Window";
569 $self->{window}->set_title("KGS Game $title");
570 ::state $self->{window}, "game::window", undef, window_size => [600, 500];
571
572 $self->{window}->signal_connect(delete_event => sub { $self->part; 1 });
573
574 $self->{window}->add(my $hpane = new Gtk::HPaned);
575 ::state $hpane, "game::hpane", undef, hpane_position => 500;
576
577 $hpane->pack1(my $vbox = new Gtk::VBox);
578
579 $vbox->pack_start((my $frame = new Gtk::Frame), 0, 1, 0);
580
581 {
582 $frame->add(my $vbox = new Gtk::VBox);
583 $vbox->add($self->{title} = new Gtk::Label $title);
584
585 $self->{moveadj} = new Gtk::Adjustment 0, 0, 0, 1, 10, 0;
586 $vbox->add(my $scale = new Gtk::HScale $self->{moveadj});
587 $scale->set_draw_value (1);
588 $scale->set_digits (0);
589 $scale->set_value_pos('top');
590
591 $self->{moveadj}->signal_connect (value_changed => sub {
592 $self->{board} = new KGS::Game::Board $self->{size};
593 $self->{board}->interpret_path ([@{$self->{path}}[0 .. $self->{moveadj}->value - 1]]);
594
595 my $area = $self->repaint_board;
596
597 # force a redraw (not perfect(?))
598 $self->expose ($area);
599
600 $self->{text}->backward_delete($self->{text}->get_length);
601 $self->{text}->insert(undef, undef, undef, KGS::Listener::Debug::dumpval([$self->{board}{time},$self->{board}{captures}]). $self->{board}{comment});
602 });
603
604 $self->{moveadj}->upper (scalar @{$self->{path}}) if $self->{path};
605 }
606
607 Gtk::Widget->push_visual (Gtk::Gdk::Rgb->get_visual);
608 Gtk::Widget->push_colormap (Gtk::Gdk::Rgb->get_cmap);
609 $vbox->pack_start(($self->{canvas} = new Gtk::DrawingArea), 1, 1, 0);
610 Gtk::Widget->pop_colormap;
611 Gtk::Widget->pop_visual;
612
613 $self->{canvas}->signal_connect(configure_event => \&configure_event, $self);
614 $self->{canvas}->signal_connect(expose_event => \&expose_event, $self);
615
616 $hpane->pack2((my $vpane = new Gtk::VPaned), 0, 0);
617 ::state $vpane, "game", $self->{name}, vpane_position => 80;
618
619 $vpane->add(my $sw = new Gtk::ScrolledWindow);
620 $sw->set_policy("automatic", "always");
621
622 $sw->add($self->{userlist} = new_with_titles Gtk::CList "User", "Rank", "Flags");
623 ::clist_autosort $self->{userlist};
624 ::state $self->{userlist}, "room::userlist", $self->{name}, clist_column_widths => [120, 30];
625
626 $vpane->add(my $vbox = new Gtk::VBox);
627
628 $vbox->pack_start((my $sw = new Gtk::ScrolledWindow), 1, 1, 0);
629 $sw->set_policy("automatic", "always");
630
631 $sw->add($self->{text} = new Gtk::Text);
632
633 $vbox->pack_start(($self->{entry} = new Gtk::Entry), 0, 1, 0);
634 $self->{entry}->signal_connect(activate => sub {
635 my $text = $self->{entry}->get_text;
636 # add message
637 $self->{entry}->set_text("");
638 });
639
640 $self;
641 }
642
643 sub event_update_users {
644 my ($self) = @_;
645
646 room::event_update_users $self;
647 }
648
649 sub join {
650 my ($self) = @_;
651 $self->SUPER::join;
652
653 $self->{window}->show_all;
654 }
655
656 sub part {
657 my ($self) = @_;
658 $self->SUPER::part;
659
660 $self->{window}->hide;
661 }
662
663 sub configure_event {
664 my ($widget, $self, $event) = @_;
665 delete $self->{stack};
666 delete $self->{pixbuf};
667 delete $self->{board_shown};
668 delete $self->{background};
669 $self->repaint_board;
670 1;
671 }
672
673 sub INTERP_NEAREST (){ 1 }
674 sub INTERP_TILES (){ 1 }
675 sub INTERP_BILINEAR (){ 2 }
676 sub INTERP_HYPER (){ 3 }
677
678 sub new_pixbuf {
679 my ($w, $h, $alpha, $clear) = @_;
680
681 my $pixbuf = new Gtk::Gdk::Pixbuf 'rgb', $alpha, 8, $w, $h;
682
683 if ($clear) { # damn, need to clear it ourselves
684 my $row = "\x00\x00\x00\x00" x $w;
685 $pixbuf->put_pixels ($row, $_, 0) for 0 .. $h - 1;
686 }
687
688 $pixbuf;
689 }
690
691 sub scale_pixbuf {
692 my ($src, $w, $h, $mode) = @_;
693
694 my $dst = new_pixbuf $w, $h, 1;
695
696 $src->scale(
697 $dst, 0, 0, $w, $h, 0, 0,
698 $w / $src->get_width, $h / $src->get_height,
699 $mode,
700 );
701
702 $dst;
703 }
704
705 # create a stack of stones
706 sub create_stack {
707 my ($self, $mark, $size, $rand) = @_;
708
709 my $shadow = $size * 0.05;
710
711 my $c = \$self->{stack}{$mark};
712 unless ($$c) {
713 for my $stone ($mark & (MARK_W | MARK_GRAY_W) ? @::white_img : @::black_img) {
714 my $base = new_pixbuf $size + $shadow, $size + $shadow, 1, 1;
715
716 # zeroeth the shadow
717 if ($mark & (MARK_B | MARK_W)) {
718 $::black_img[0]->composite (
719 $base, $shadow, $shadow, $size, $size, $shadow-0.5, $shadow-0.5,
720 $size / $stone->get_width, $size / $stone->get_height,
721 $::config->{speed} ? INTERP_NEAREST : INTERP_TILES, 128
722 );
723 }
724
725 # first the big stones (handicap stones different for effect)
726 for ([MARK_B, $mark & MARK_MOVE ? 255 : 192],
727 [MARK_W, $mark & MARK_MOVE ? 255 : 192],
728 [MARK_GRAY_B, 128],
729 [MARK_GRAY_W, 128]) {
730 my ($mask, $alpha) = @$_;
731 if ($mark & $mask) {
732 $stone->composite (
733 $base, 0, 0, $size, $size, -0.5, -0.5,
734 $size / $stone->get_width, $size / $stone->get_height,
735 $::config->{speed} ? INTERP_NEAREST : INTERP_HYPER, $alpha
736 );
737 }
738 }
739
740 # then the samll stones
741 for ([MARK_SMALL_B, $::black_img[$rand % @::black_img]],
742 [MARK_SMALL_W, $::white_img[$rand % @::white_img]]) {
743 my ($mask, $img) = @$_;
744 if ($mark & $mask) {
745 $img->composite (
746 $base, ($size / 4) x2, (int ($size / 2 + 0.5)) x2, ($size / 4 - 0.5) x 2,
747 $size / $img->get_width / 2, $size / $img->get_height / 2,
748 $::config->{speed} ? INTERP_NEAREST : INTERP_HYPER, 192
749 );
750 }
751 }
752
753 # and lastly any markers
754 my $dark_bg = ! ! ($mark & (MARK_B | MARK_GRAY_B));
755
756 for ([MARK_CIRCLE, $::circle_img[$dark_bg]],
757 [MARK_TRIANGLE, $::triangle_img[$dark_bg]],
758 [MARK_SQUARE, $::square_img[$dark_bg]]) {
759 my ($mask, $img) = @$_;
760 if ($mark & $mask) {
761 $img->composite (
762 $base, 0, 0, $size, $size, -0.5, -0.5,
763 $size / $img->get_width, $size / $img->get_height,
764 $::config->{speed} ? INTERP_NEAREST : INTERP_HYPER, 255
765 );
766 }
767 }
768
769 push @$$c, $base;
770 }
771 }
772
773 $$c->[$rand % @$$c];
774 }
775
776 sub pixbuf_text {
777 my ($pixbuf, $colour, $x, $y, $height, $text) = @_;
778
779 my @c = grep $_,
780 map $::font[$colour][$::fontmap{$_}],
781 split //, $text;
782
783 if (@c) {
784 my $spacing = $height * 0.1;
785 my $s = $height / List::Util::max map $_->get_height, @c;
786 my $W = List::Util::sum map $_->get_width, @c;
787
788 $x -= ($W * $s + $spacing * (@c - 1)) * 0.5;
789 $y -= $height * 0.5;
790
791 for (@c) {
792 my $w = $_->get_width * $s;
793 $_->composite ($pixbuf,
794 $x, $y, $w+0.999, $height+0.999, $x, $y, $s, $s,
795 $::config->{speed} ? INTERP_NEAREST : INTERP_BILINEAR, 255);
796
797 $x += $w + $spacing;
798 }
799 }
800 }
801
802 sub pixbuf_rect {
803 my ($pb, $colour, $x1, $y1, $x2, $y2, $alpha) = @_;
804 # we fake lines by... an unspeakable method :/
805 my $colour_pb = new_pixbuf 1, 1, 0, 0;
806 $colour_pb->put_pixels ($colour, 0, 0);
807
808 $colour_pb->composite ($pb, $x1, $y1, $x2 - $x1 + 1, $y2 - $y1 + 1, $x1, $y1, 1, 1, INTERP_NEAREST, $alpha);
809 }
810
811 sub repaint_board {
812 my ($self) = @_;
813 my $canvas = $self->{canvas};
814 my $expose_area = undef;
815
816 return $expose_area unless $self->{board};
817
818 my ($w, $h) = @{$canvas->allocation}[2,3];
819
820 my $s = $w > $h ? $h : $w;
821
822 $self->{offsets} = [int (($w - $s) / 2), int (($h - $s) / 2), $s];
823
824 my $size = $self->{size};
825
826 my $border = int ($s / ($size + 3) * 0.5);
827 my $s2 = $s - $border * 2;
828 my $edge = int ($s2 / ($size + 1) * 0.95) - ($::config->{randomize} ? 3 : 0);
829 my $ofs = int ($edge / 2);
830
831 my @k = map int ($s2 * $_ / ($size+1) + $border + 0.5), 0 .. $size;
832
833 my $pixbuf;
834
835 my $oldboard;
836
837 if ($self->{background}) {
838 if ($oldboard = $self->{board_shown}) {
839 $pixbuf = $self->{pixbuf};
840 } else {
841 $pixbuf = $self->{background}->copy;
842 $expose_area = [0, 0, $s, $s];
843 }
844 } else {
845 $expose_area = [0, 0, $s, $s];
846
847 my ($bw, $bh) = ($::board_img->get_width, $::board_img->get_height);
848
849 if ($s < $bw && $s < $bh) {
850 $pixbuf = new_pixbuf $s, $s, $::config->{conserve_memory} ? 0 : 1, 0;
851 $::board_img->copy_area (0, 0, $s, $s, $pixbuf, 0, 0);
852 } else {
853 $pixbuf = scale_pixbuf $::board_img, $s, $s, $::config->{speed} ? INTERP_NEAREST : INTERP_TILES;
854 }
855
856 my $linew = int ($s / 25 / $size);
857
858 # ornamental border... we have time to waste :/
859 pixbuf_rect $pixbuf, "\xff\xcc\x77", 0, 0, $s-1, $linew, 255;
860 pixbuf_rect $pixbuf, "\xff\xcc\x77", 0, 0, $linew, $s-1, 255;
861 pixbuf_rect $pixbuf, "\xff\xcc\x77", $s-$linew-1, 0, $s-1, $s-1, 255;
862 pixbuf_rect $pixbuf, "\xff\xcc\x77", 0, $s-$linew-1, $s-1, $s-1, 255;
863
864 for my $i (1 .. $size) {
865 pixbuf_rect $pixbuf, "\x44\x11\x11", $k[$i] - $linew, $k[1] - $linew, $k[$i] + $linew, $k[$size] + $linew, 192;
866 pixbuf_rect $pixbuf, "\x44\x11\x11", $k[1] - $linew, $k[$i] - $linew, $k[$size] + $linew, $k[$i] + $linew, 192;
867
868 # 38 max, but we allow a bit more
869 my $label = (qw(- A B C D E F G H J K L M N O P Q R S T U V W X Y Z
870 AA BB CC DD EE FF GG HH JJ KK LL MM NN OO PP QQ RR SS TT UU VV WW XX YY ZZ))[$i];
871
872 pixbuf_text $pixbuf, 0, $k[$i], $border, $ofs, $label;
873 pixbuf_text $pixbuf, 0, $k[$i], $s2 + $border, $ofs, $label;
874 pixbuf_text $pixbuf, 0, $border, $k[$i], $ofs, $size - $i + 1;
875 pixbuf_text $pixbuf, 0, $s2 + $border, $k[$i], $ofs, $size - $i + 1;
876
877 $a++;
878 $a++ if $a eq "I"; # not correct, instead of AA AB, we should get HH JJ KK...
879 }
880
881 unless ($::config->{conserve_memory}) {
882 $self->{background} = $pixbuf;
883 $pixbuf = $pixbuf->copy;
884 }
885 }
886
887 $self->{pixbuf} = $pixbuf;
888
889 # hoshi-points(!)#d#
890 # caching of empty board gfx(!)#d#
891
892 for my $x (1 .. $size) {
893 for my $y (1 .. $size) {
894 my $rand = ($x ^ $y ^ 0x5555);
895
896 my ($dx, $dy) = ($k[$x] - $ofs, $k[$y] - $ofs);
897
898 if ($::config->{randomize}) {
899 $dx += ($rand % 7) - 3;
900 $dy += ($rand / 3 % 7) - 3;
901 }
902
903 my $shadow = $edge * 0.05;
904 my $area = [$dx, $dy, $edge + $shadow, $edge + $shadow];
905
906 my $mark = $self->{board}{board}[$x-1][$y-1];
907 my $old = $oldboard ? $oldboard->{board}[$x-1][$y-1] : 0;
908
909 if ($oldboard) {
910 next if $old == $mark; # no change
911
912 $self->{background}->copy_area (@$area, $pixbuf, $dx, $dy);
913 $expose_area = $expose_area
914 ? Gtk::Gdk::Rectangle->union ($expose_area, $area)
915 : $area;
916 }
917
918 if ($mark) {
919 my $pb = $self->create_stack($mark, $edge, $rand);
920
921 $pb->composite ($pixbuf, @$area,
922 $dx, $dy, 1, 1, $::config->{speed} ? INTERP_NEAREST : INTERP_NEAREST, 255);
923
924 # labels are handled here because they are quite rare
925 if ($mark & MARK_LABEL) {
926 my $white = $mark & (MARK_W | MARK_GRAY_W) ? 0 : 1;
927
928 if ($white) {
929 pixbuf_text $pixbuf, 0,
930 $k[$x] + $ofs * 0.1, $k[$y] + $ofs * 0.1, $ofs * 0.7,
931 $self->{board}{label}[$x-1][$y-1];
932 }
933 pixbuf_text $pixbuf, $white,
934 $k[$x], $k[$y], $ofs * 0.7,
935 $self->{board}{label}[$x-1][$y-1];
936 }
937
938 # old pixmap&mask-way. that was fast ;(
939 #my ($pm, $bm) = $self->create_stack($gc, $mark, $edge, $x * 17 + $y * 11 );
940
941 #$gc->set_clip_mask ($bm);
942 #$gc->set_clip_origin ($dx, $dy);
943 #$pixmap->draw_pixmap ($gc, $pm, 0, 0, $dx, $dy, $edge, $edge);
944 }
945 }
946 }
947
948 $self->{board_shown} = Storable::dclone $self->{board};
949 #d# save
950 #Storable::nstore { board => $self->{board}, size => $self->{size}, path => $self->{path}}, "testboard.storable";
951
952 $expose_area;
953 }
954
955 sub expose {
956 my ($self, $area) = @_;
957
958 if ($area && $self->{pixbuf}) {
959 my ($x, $y, $w, $h) = @$area;
960 my ($ox, $oy, $s) = @{$self->{offsets}};
961
962 $self->{pixbuf}->render_to_drawable ($self->{canvas}->window, $self->{canvas}->style->white_gc,
963 $x, $y, $x + $ox, $y + $oy, $w, $h);
964 $self->{canvas}->window->draw_rectangle ($self->{canvas}->style->black_gc, 0,
965 $x + $ox - 1, $y + $oy - 1, $w + 2, $h + 2) if $::DEBUG_EXPOSE;
966
967 #$self->{canvas}->window->draw_pixmap (
968 # $self->{canvas}->style->white_gc,
969 # $self->{pixmap},
970 # $x - $ox, $y - $oy, $x, $y, $w, $h,
971 #);
972 }
973 }
974
975 sub expose_event {
976 my ($widget, $self, $event) = @_;
977
978 $self->{pixbuf} or return;
979
980 my $area = $event->{area};
981 my ($ox, $oy, $s) = @{$self->{offsets}};
982
983 $self->expose (Gtk::Gdk::Rectangle->intersect (
984 [$area->[0] - $ox, $area->[1] - $oy, $area->[2], $area->[3]],
985 [0, 0, $s, $s],
986 ));
987
988 1;
989 }
990
991 sub event_update_tree {
992 my ($self) = @_;
993
994 $self->{path} = $self->get_path;
995
996 my $move = @{$self->{path}};
997
998 $self->{moveadj}->upper($move);
999
1000 if ($self->{moveadj}->value >= $move - 1 || !$self->{moveadj}->value) {
1001 $self->{moveadj}->value ($move);
1002 $self->{moveadj}->value_changed;
1003 }
1004 }
1005
1006 sub event_part {
1007 my ($self) = @_;
1008 $self->SUPER::event_part;
1009 (delete $self->{window})->destroy; # hmm.. why does this keep the object alive? puzzling.. ahh.. the callbacks ;)
1010 delete $self->{room}{game}{$self->{channel}};
1011 }
1012
1013 sub event_move {
1014 my ($self, $pass) = @_;
1015 ::play_sound 1, $pass ? "pass" : "move";
1016 }
1017
1018 sub DESTROY {#d#
1019 warn "DESTROY(@_)\n";#d#
1020 }
1021
1022 1;
1023
1024
1025