ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/kgsueme/kgsueme/chat.pl
Revision: 1.12
Committed: Tue Jun 8 17:35:00 2004 UTC (19 years, 11 months ago) by root
Content type: text/plain
Branch: MAIN
Changes since 1.11: +75 -26 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 use utf8;
2
3 package chat;
4
5 # waaay cool widget. well... maybe at one point in the future
6
7 use Gtk2;
8 use Gtk2::Pango;
9
10 use Glib::Object::Subclass
11 Gtk2::VBox,
12 signals => {
13 command => {
14 flags => [qw/run-first/],
15 return_type => undef,
16 param_types => [Glib::Scalar, Glib::Scalar],
17 class_closure => sub { },
18 },
19 tag_event => {
20 flags => [qw/run-first/],
21 return_type => undef,
22 # tag, event, content
23 param_types => [Glib::String, Gtk2::Gdk::Event, Glib::String],
24 class_closure => sub { },
25 },
26 enter_tag => {
27 flags => [qw/run-first/],
28 return_type => undef,
29 # tag, content
30 param_types => [Glib::String, Glib::String],
31 class_closure => sub { },
32 },
33 leave_tag => {
34 flags => [qw/run-first/],
35 return_type => undef,
36 # tag
37 param_types => [Glib::String, Glib::String],
38 class_closure => sub { },
39 },
40 };
41
42 sub INIT_INSTANCE {
43 my $self = shift;
44
45 my $tagtable = new Gtk2::TextTagTable;
46
47 {
48 my @tags = (
49 [default => { foreground => "black", wrap_mode => "word-char" }],
50 [node => { foreground => "#0000b0", event => 1 }],
51 [move => { foreground => "#0000b0", event => 1 }],
52 [user => { foreground => "#0000b0", event => 1 }],
53 [coord => { foreground => "#0000b0", event => 1 }],
54 [score => { foreground => "#0000b0", event => 1 }],
55 [error => { foreground => "#ff0000", event => 1 }],
56 [leader => { weight => 800, pixels_above_lines => 6 }],
57 [header => { weight => 800, pixels_above_lines => 6 }],
58 [undo => { foreground => "#ffff00", background => "#ff0000", weight => 800, pixels_above_lines => 6 }],
59 [challenge => { weight => 800, pixels_above_lines => 6, background => "#ffffb0" }],
60 [description => { weight => 800, foreground => "blue" }],
61 [infoblock => { weight => 700, foreground => "blue" }],
62 );
63
64 for (@tags) {
65 my ($k, $v) = @$_;
66 my $tag = new Gtk2::TextTag $k;
67 if (delete $v->{event}) {
68 $tag->signal_connect (event => sub {
69 my ($tag, $view, $event, $iter) = @_;
70 my ($a, $b) = ($iter, $iter->copy);
71 $a->backward_to_tag_toggle ($tag) unless $a->begins_tag ($tag);
72 $b->forward_to_tag_toggle ($tag) unless $b->ends_tag ($tag);
73
74 $self->signal_emit (tag_event => $k, $event, $a->get_text ($b));
75 });
76 }
77 $tag->set (%$v);
78 $tagtable->add ($tag);
79 }
80 }
81
82 $self->{tagtable} = $tagtable;
83
84 $self->signal_connect (destroy => sub {
85 remove Glib::Source delete $self->{idle} if $self->{idle};
86 %{$_[0]} = ();
87 });
88
89 $self->{buffer} = new Gtk2::TextBuffer $self->{tagtable};
90
91 $self->{widget} = new Gtk2::ScrolledWindow;
92 $self->{widget}->set_policy ("automatic", "always");
93 $self->pack_start ($self->{widget}, 1, 1, 0);
94
95 $self->{widget}->add ($self->{view} = new_with_buffer Gtk2::TextView $self->{buffer});
96 $self->{view}->set (
97 wrap_mode => "word-char",
98 cursor_visible => 0,
99 editable => 0,
100 tabs =>
101 (new Gtk2::Pango::TabArray 1, 0, left => 125000), # arbitrary... pango is underfeatured
102 );
103
104 $self->{view}->signal_connect (motion_notify_event => sub {
105 my ($widget, $event) = @_;
106
107 my $window = $widget->get_window ("text");
108 if ($event->window == $window) {
109 my ($win, $x, $y, $mask) = $window->get_pointer;
110 ($x, $y) = $self->{view}->window_to_buffer_coords ("text", $x, $y);
111 my ($iter) = $self->{view}->get_iter_at_location ($x, $y);
112
113 my $tag = ($iter->get_tags)[01];
114
115 if ($tag) {
116 my ($a, $b) = ($iter, $iter->copy);
117 $a->backward_to_tag_toggle ($tag) unless $a->begins_tag ($tag);
118 $b->forward_to_tag_toggle ($tag) unless $b->ends_tag ($tag);
119
120 $self->tag_enterleave ($tag->get ("name"), $a->get_text ($b));
121 } else {
122 $self->tag_enterleave ();
123 }
124
125 1;
126 }
127 0;
128 });
129
130 $self->{view}->signal_connect (leave_notify_event => sub {
131 $self->tag_enterleave ();
132 0;
133 });
134
135 $self->{view}->add_events (qw(leave_notify_mask));
136
137 $self->pack_start (($self->{entry} = new Gtk2::Entry), 0, 1, 0);
138
139 $self->{entry}->signal_connect (activate => sub {
140 my ($entry) = @_;
141 my $text = $entry->get_text;
142 $entry->set_text("");
143
144 my ($cmd, $arg);
145
146 if ($text =~ /^\/(\S+)\s*(.*)$/) {
147 ($cmd, $arg) = ($1, $2);
148 } else {
149 ($cmd, $arg) = ("say", $text);
150 }
151
152 $self->signal_emit (command => $cmd, $arg);
153 });
154
155 #$self->{end} = $self->{buffer}->create_mark (undef, $self->{buffer}->get_end_iter, 0);#d##todo# use this one for gtk-1.050+
156 $self->{end} = $self->{buffer}->create_mark (++$USELESSNAME, $self->{buffer}->get_end_iter, 0); # workaround for gtk-perl bug
157
158 $self->set_end;
159 }
160
161 sub tag_enterleave {
162 my ($self, $tag, $content) = @_;
163
164 my $cur = $self->{current_tag};
165
166 if ($cur->[0] != $tag || $cur->[1] ne $content) {
167 $self->signal_emit (leave_tag => @$cur) if $cur->[0];
168 $self->{current_tag} = $cur = [$tag, $content];
169 $self->signal_emit (enter_tag => @$cur) if $cur->[0];
170 }
171 }
172
173 sub set_end {
174 my ($self) = @_;
175
176 # this is probably also a hack...
177 $self->{idle} ||= add Glib::Idle sub {
178 $self->{view}->scroll_to_iter ($self->{buffer}->get_end_iter, 0, 0, 0, 0)
179 if $self->{view};
180 delete $self->{idle};
181 };
182 }
183
184 sub at_end {
185 my ($self) = @_;
186
187 # this is, maybe, a bad hack :/
188 my $adj = $self->{widget}->get_vadjustment;
189 $adj->value + $adj->page_size >= $adj->upper - 0.5;
190 }
191
192 sub append_text {
193 my ($self, $text) = @_;
194
195 $self->_append_text ($self->{end}, $text);
196 }
197
198 sub _append_text {
199 my ($self, $mark, $text) = @_;
200
201 my $at_end = $self->at_end;
202
203 $text = "<default>$text</default>";
204
205 my @tag;
206 # pseudo-simplistic-xml-parser
207 for (;;) {
208 $text =~ /\G<([^>]+)>/gc or last;
209 my $tag = $1;
210 if ($tag =~ s/^\///) {
211 pop @tag;
212 } else {
213 push @tag, $tag;
214 }
215
216 $text =~ /\G([^<]*)/gc or last;
217 $self->{buffer}->insert_with_tags_by_name ($self->{buffer}->get_iter_at_mark ($mark), util::xmlto $1, @tag)
218 if length $1;
219 }
220
221 $self->set_end if $at_end;
222 }
223
224 sub set_text {
225 my ($self, $text) = @_;
226
227 my $at_end = $self->at_end;
228
229 $self->{buffer}->set_text ("");
230 $self->append_text ($text);
231
232 $self->set_end if $at_end;
233 }
234
235 sub new_eventtag {
236 my ($self, $cb) = @_;
237
238 my $tag = new Gtk2::TextTag;
239 $tag->signal_connect (event => $cb);
240 $self->{tagtable}->add ($tag);
241
242 $tag
243 }
244
245 # create a new "subbuffer"
246 sub new_inlay {
247 my ($self) = @_;
248
249 my $end = $self->{buffer}->get_end_iter;
250
251 my $self = bless {
252 buffer => $self->{buffer},
253 parent => $self,
254 }, superchat::inlay;
255
256 # $USELESSNAME is a Gtk-perl < 1.042 workaround
257 $self->{l} = $self->{buffer}->create_mark (++$USELESSNAME, $end, 1);
258 $self->{buffer}->insert ($end, "\x{200d}");
259 $self->{r} = $self->{buffer}->create_mark (++$USELESSNAME, $self->{buffer}->get_iter_at_mark ($self->{l}), 0);
260
261 Scalar::Util::weaken $self->{buffer};
262 Scalar::Util::weaken $self->{parent};
263 $self;
264 }
265
266 sub new_switchable_inlay {
267 my ($self, $header, $cb, $visible) = @_;
268
269 my $inlay;
270
271 my $tag = $self->new_eventtag (sub {
272 my ($tag, $view, $event, $iter) = @_;
273
274 if ($event->type eq "button-press") {
275 $inlay->set_visible (!$inlay->{visible});
276 return 1;
277 }
278
279 0;
280 });
281
282 $tag->set (background => "#e0e0ff");
283
284 $inlay = $self->new_inlay;
285
286 $inlay->{visible} = $visible;
287 $inlay->{header} = $header;
288 $inlay->{tag} = $tag;
289 $inlay->{cb} = $cb;
290
291 Scalar::Util::weaken $inlay->{tag};
292
293 $inlay->refresh;
294
295 $inlay;
296 }
297
298 package superchat::inlay;
299
300 sub liter { $_[0]{buffer}->get_iter_at_mark ($_[0]{l}) }
301 sub riter { $_[0]{buffer}->get_iter_at_mark ($_[0]{r}) }
302
303 sub clear {
304 my ($self) = @_;
305 $self->{buffer}->delete ($self->liter, $self->riter);
306 }
307
308 sub append_text {
309 my ($self, $text) = @_;
310
311 $self->{parent}->_append_text ($self->{r}, $text);
312 }
313
314 sub append_widget {
315 my ($self, $widget) = @_;
316
317 $widget->show_all;
318
319 my $anchor = $self->{buffer}->create_child_anchor ($self->riter);
320 $self->{parent}{view}->add_child_at_anchor ($widget, $anchor);
321
322 $widget;
323 }
324
325 sub append_optionmenu {
326 my ($self, $ref, @entry) = @_;
327
328 $self->append_widget (gtk::optionmenu $ref, @entry);
329 }
330
331 sub append_button {
332 my ($self, $label, $cb) = @_;
333
334 $self->append_widget (gtk::button $label, $cb);
335 }
336
337 sub visible { $_[0]{visible} }
338
339 sub set_visible {
340 my ($self, $visible) = @_;
341
342 return if $self->{visible} == $visible;
343 $self->{visible} = $visible;
344
345 $self->refresh;
346 }
347
348 sub refresh {
349 my ($self) = @_;
350
351 $self->clear;
352
353 my $arrow = $self->{visible} ? "⊟" : "⊞";
354
355 $self->{buffer}->insert ($self->riter, "\n");
356 $self->{buffer}->insert_with_tags ($self->riter, util::xmlto "$arrow $self->{header}", $self->{tag});
357
358 return unless $self->{visible};
359
360 $self->{cb}->($self);
361 }
362
363 sub destroy {
364 my ($self) = @_;
365
366 return if !$self->{l} || !$self->{buffer} || $self->{l}->get_deleted;
367
368 $self->clear if $self->{buffer};
369
370 delete $self->{parent};
371 delete $self->{buffer};
372 delete $self->{l};
373 delete $self->{r};
374 }
375
376 sub DESTROY {
377 my $self = shift;
378
379 $self->{parent}{tagtable}->remove (delete $self->{tag}) if $self->{tag} && $self->{parent};
380 #&destroy;
381 }
382
383 1;
384