ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/kgsueme/kgsueme/chat.pl
Revision: 1.14
Committed: Tue Jun 8 21:33:08 2004 UTC (19 years, 11 months ago) by root
Content type: text/plain
Branch: MAIN
Changes since 1.13: +1 -1 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 # this is probably also a hack...
204 $self->{idle} ||= add Glib::Idle sub {
205 $self->{view}->scroll_to_iter ($self->{buffer}->get_end_iter, 0, 0, 0, 0)
206 if $self->{view};
207 delete $self->{idle};
208 };
209 }
210
211 sub at_end {
212 my ($self) = @_;
213
214 # this is, maybe, a bad hack :/
215 my $adj = $self->{widget}->get_vadjustment;
216 $adj->value + $adj->page_size >= $adj->upper - 0.5;
217 }
218
219 sub append_text {
220 my ($self, $text) = @_;
221
222 $self->_append_text ($self->{end}, $text);
223 }
224
225 sub _append_text {
226 my ($self, $mark, $text) = @_;
227
228 my $at_end = $self->at_end;
229
230 $text = "<default>$text</default>";
231
232 my @tag;
233 # pseudo-simplistic-xml-parser
234 for (;;) {
235 $text =~ /\G<([^>]+)>/gc or last;
236 my $tag = $1;
237 if ($tag =~ s/^\///) {
238 pop @tag;
239 } else {
240 push @tag, $tag;
241 }
242
243 $text =~ /\G([^<]*)/gc or last;
244 $self->{buffer}->insert_with_tags_by_name ($self->{buffer}->get_iter_at_mark ($mark), util::xmlto $1, @tag)
245 if length $1;
246 }
247
248 $self->set_end if $at_end;
249 }
250
251 sub set_text {
252 my ($self, $text) = @_;
253
254 my $at_end = $self->at_end;
255
256 $self->{buffer}->set_text ("");
257 $self->append_text ($text);
258
259 $self->set_end if $at_end;
260 }
261
262 sub new_eventtag {
263 my ($self, $cb) = @_;
264
265 my $tag = new Gtk2::TextTag;
266 $tag->signal_connect (event => $cb);
267 $self->{tagtable}->add ($tag);
268
269 $tag
270 }
271
272 # create a new "subbuffer"
273 sub new_inlay {
274 my ($self) = @_;
275
276 my $end = $self->{buffer}->get_end_iter;
277
278 my $self = bless {
279 buffer => $self->{buffer},
280 parent => $self,
281 }, superchat::inlay;
282
283 # $USELESSNAME is a Gtk-perl < 1.042 workaround
284 $self->{l} = $self->{buffer}->create_mark (++$USELESSNAME, $end, 1);
285 $self->{buffer}->insert ($end, "\x{200d}");
286 $self->{r} = $self->{buffer}->create_mark (++$USELESSNAME, $self->{buffer}->get_iter_at_mark ($self->{l}), 0);
287
288 Scalar::Util::weaken $self->{buffer};
289 Scalar::Util::weaken $self->{parent};
290 $self;
291 }
292
293 sub new_switchable_inlay {
294 my ($self, $header, $cb, $visible) = @_;
295
296 my $inlay;
297
298 my $tag = $self->new_eventtag (sub {
299 my ($tag, $view, $event, $iter) = @_;
300
301 if ($event->type eq "button-press") {
302 $inlay->set_visible (!$inlay->{visible});
303 return 1;
304 }
305
306 0;
307 });
308
309 $tag->set (background => "#e0e0ff");
310
311 $inlay = $self->new_inlay;
312
313 $inlay->{visible} = $visible;
314 $inlay->{header} = $header;
315 $inlay->{tag} = $tag;
316 $inlay->{cb} = $cb;
317
318 Scalar::Util::weaken $inlay->{tag};
319
320 $inlay->refresh;
321
322 $inlay;
323 }
324
325 package superchat::inlay;
326
327 sub liter { $_[0]{buffer}->get_iter_at_mark ($_[0]{l}) }
328 sub riter { $_[0]{buffer}->get_iter_at_mark ($_[0]{r}) }
329
330 sub clear {
331 my ($self) = @_;
332 $self->{buffer}->delete ($self->liter, $self->riter);
333 }
334
335 sub append_text {
336 my ($self, $text) = @_;
337
338 $self->{parent}->_append_text ($self->{r}, $text);
339 }
340
341 sub append_widget {
342 my ($self, $widget) = @_;
343
344 $widget->show_all;
345
346 my $anchor = $self->{buffer}->create_child_anchor ($self->riter);
347 $self->{parent}{view}->add_child_at_anchor ($widget, $anchor);
348
349 $widget;
350 }
351
352 sub append_optionmenu {
353 my ($self, $ref, @entry) = @_;
354
355 $self->append_widget (gtk::optionmenu $ref, @entry);
356 }
357
358 sub append_button {
359 my ($self, $label, $cb) = @_;
360
361 $self->append_widget (gtk::button $label, $cb);
362 }
363
364 sub visible { $_[0]{visible} }
365
366 sub set_visible {
367 my ($self, $visible) = @_;
368
369 return if $self->{visible} == $visible;
370 $self->{visible} = $visible;
371
372 $self->refresh;
373 }
374
375 sub refresh {
376 my ($self) = @_;
377
378 $self->clear;
379
380 my $arrow = $self->{visible} ? "⊟" : "⊞";
381
382 $self->{buffer}->insert ($self->riter, "\n");
383 $self->{buffer}->insert_with_tags ($self->riter, util::xmlto "$arrow $self->{header}", $self->{tag});
384
385 return unless $self->{visible};
386
387 $self->{cb}->($self);
388 }
389
390 sub destroy {
391 my ($self) = @_;
392
393 return if !$self->{l} || !$self->{buffer} || $self->{l}->get_deleted;
394
395 $self->clear if $self->{buffer};
396
397 delete $self->{parent};
398 delete $self->{buffer};
399 delete $self->{l};
400 delete $self->{r};
401 }
402
403 sub DESTROY {
404 my $self = shift;
405
406 $self->{parent}{tagtable}->remove (delete $self->{tag}) if $self->{tag} && $self->{parent};
407 #&destroy;
408 }
409
410 1;
411