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