|
|
1 | use utf8; |
|
|
2 | |
1 | package chat; |
3 | package chat; |
2 | |
4 | |
|
|
5 | # waaay cool widget. well... maybe at one point in the future |
|
|
6 | |
3 | use Gtk2; |
7 | use Gtk2; |
|
|
8 | use Gtk2::Pango; |
4 | |
9 | |
5 | use Glib::Object::Subclass |
10 | use Glib::Object::Subclass |
6 | Gtk2::VBox, |
11 | Gtk2::VBox, |
7 | signals => { |
12 | signals => { |
8 | command => { |
13 | command => { |
9 | flags => [qw/run-first/], |
14 | flags => [qw/run-last/], |
10 | return_type => undef, # void return |
15 | return_type => undef, |
11 | param_types => [Glib::Scalar, Glib::Scalar], |
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 { }, |
12 | }, |
39 | }, |
13 | }; |
40 | }; |
14 | |
41 | |
15 | my $tagtable = new Gtk2::TextTagTable; |
42 | sub new { |
|
|
43 | my ($self, %arg) = @_; |
16 | |
44 | |
17 | { |
45 | $self = $self->Glib::Object::new; |
18 | my %tags = ( |
46 | $self->{$_} = delete $arg{$_} for keys %arg; |
19 | default => { foreground => "black" }, |
|
|
20 | node => { foreground => "#0000b0", event => 1 }, |
|
|
21 | move => { foreground => "#0000b0", event => 1 }, |
|
|
22 | user => { foreground => "#0000b0", event => 1 }, |
|
|
23 | coord => { foreground => "#0000b0", event => 1 }, |
|
|
24 | error => { foreground => "#ff0000", event => 1 }, |
|
|
25 | header => { weight => 800, pixels_above_lines => 6 }, |
|
|
26 | description => { weight => 800, foreground => "blue" }, |
|
|
27 | infoblock => { weight => 700, foreground => "blue" }, |
|
|
28 | ); |
|
|
29 | |
47 | |
30 | while (my ($k, $v) = each %tags) { |
48 | $self; |
31 | my $tag = new Gtk2::TextTag $k; |
|
|
32 | if (delete $v->{event}) { |
|
|
33 | ### |
|
|
34 | } |
|
|
35 | $tag->set (%$v); |
|
|
36 | $tagtable->add ($tag); |
|
|
37 | } |
|
|
38 | } |
49 | } |
39 | |
50 | |
40 | sub INIT_INSTANCE { |
51 | sub INIT_INSTANCE { |
41 | my $self = shift; |
52 | my $self = shift; |
42 | |
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 | |
43 | $self->{buffer} = new Gtk2::TextBuffer $tagtable; |
103 | $self->{buffer} = new Gtk2::TextBuffer $self->{tagtable}; |
44 | |
104 | |
45 | $self->{widget} = new Gtk2::ScrolledWindow; |
105 | $self->{widget} = new Gtk2::ScrolledWindow; |
46 | $self->{widget}->set_policy("never", "always"); |
106 | $self->{widget}->set_policy ("automatic", "always"); |
47 | $self->pack_start ($self->{widget}, 1, 1, 0); |
107 | $self->pack_start ($self->{widget}, 1, 1, 0); |
48 | |
108 | |
49 | $self->{widget}->add ($self->{view} = new_with_buffer Gtk2::TextView $self->{buffer}); |
109 | $self->{widget}->add ($self->{view} = new_with_buffer Gtk2::TextView $self->{buffer}); |
50 | $self->{view}->set_wrap_mode ("word"); |
|
|
51 | $self->{view}->set_cursor_visible (0); |
|
|
52 | |
|
|
53 | $self->{view}->set_editable (0); |
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 | ); |
54 | |
117 | |
55 | #use PApp::Util; warn PApp::Util::dumpval ($self->{view}->get_events); |
|
|
56 | $self->{view}->signal_connect (motion_notify_event => sub { |
118 | $self->{view}->signal_connect (motion_notify_event => sub { |
57 | my ($widget, $event) = @_; |
119 | my ($widget, $event) = @_; |
58 | |
120 | |
59 | my $window = $widget->get_window ("text"); |
121 | my $window = $widget->get_window ("text"); |
60 | if ($event->window == $window) { |
122 | if ($event->window == $window) { |
61 | my ($win, $x, $y, $mask) = $window->get_pointer; |
123 | my ($win, $x, $y, $mask) = $window->get_pointer; |
62 | # warn "TAG EVENT @_ ($window, $win, $x, $y, $mask)\n"; |
124 | ($x, $y) = $self->{view}->window_to_buffer_coords ("text", $x, $y); |
63 | #gtk_text_view_window_to_buffer_coords (text_view, |
125 | my ($iter) = $self->{view}->get_iter_at_location ($x, $y); |
64 | # GTK_TEXT_WINDOW_TEXT, |
126 | |
65 | # text_view->drag_start_x, |
127 | my $tag = ($iter->get_tags)[01]; |
66 | # text_view->drag_start_y, |
128 | |
67 | # &buffer_x, |
129 | if ($tag) { |
68 | # &buffer_y); |
130 | my ($a, $b) = ($iter, $iter->copy); |
69 | # |
131 | $a->backward_to_tag_toggle ($tag) unless $a->begins_tag ($tag); |
70 | # gtk_text_layout_get_iter_at_pixel (text_view->layout, |
132 | $b->forward_to_tag_toggle ($tag) unless $b->ends_tag ($tag); |
71 | # &iter, |
133 | |
72 | # buffer_x, buffer_y); |
134 | $self->tag_enterleave ($tag->get ("name"), $a->get_text ($b)); |
73 | # |
135 | } else { |
74 | # gtk_text_view_start_selection_dnd (text_view, &iter, event); |
136 | $self->tag_enterleave (); |
75 | # return TRUE; |
137 | } |
|
|
138 | |
|
|
139 | 1; |
76 | } |
140 | } |
77 | 0; |
141 | 0; |
78 | }); |
142 | }); |
79 | |
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 | |
80 | $self->pack_start (($self->{entry} = new Gtk2::Entry), 0, 1, 0); |
151 | $self->pack_start (($self->{entry} = new Gtk2::Entry), 0, 1, 0); |
81 | |
152 | |
82 | $self->{entry}->signal_connect(activate => sub { |
153 | $self->{entry}->signal_connect (activate => sub { |
83 | my ($entry) = @_; |
154 | my ($entry) = @_; |
84 | my $text = $entry->get_text; |
155 | my $text = $entry->get_text; |
85 | $entry->set_text(""); |
156 | $entry->set_text(""); |
86 | |
157 | |
87 | my ($cmd, $arg); |
158 | my ($cmd, $arg); |
… | |
… | |
93 | } |
164 | } |
94 | |
165 | |
95 | $self->signal_emit (command => $cmd, $arg); |
166 | $self->signal_emit (command => $cmd, $arg); |
96 | }); |
167 | }); |
97 | |
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 |
98 | |
171 | |
99 | $self->set_end; |
172 | $self->set_end; |
100 | |
|
|
101 | $self; |
|
|
102 | } |
173 | } |
103 | |
174 | |
104 | sub do_command { |
175 | sub tag_enterleave { |
105 | my ($self, $cmd, $arg, %arg) = @_; |
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 | } |
106 | } |
198 | } |
107 | |
199 | |
108 | sub set_end { |
200 | sub set_end { |
109 | my ($self) = @_; |
201 | my ($self) = @_; |
110 | |
202 | |
… | |
… | |
125 | } |
217 | } |
126 | |
218 | |
127 | sub append_text { |
219 | sub append_text { |
128 | my ($self, $text) = @_; |
220 | my ($self, $text) = @_; |
129 | |
221 | |
|
|
222 | $self->_append_text ($self->{end}, $text); |
|
|
223 | } |
|
|
224 | |
|
|
225 | sub _append_text { |
|
|
226 | my ($self, $mark, $text) = @_; |
|
|
227 | |
130 | my $at_end = $self->at_end; |
228 | my $at_end = $self->at_end; |
131 | |
229 | |
|
|
230 | $text = "<default>$text</default>"; |
|
|
231 | |
132 | my @tag; |
232 | my @tag; |
133 | $text = "<default>$text</default>"; |
|
|
134 | |
|
|
135 | # pseudo-simplistic-xml-parser |
233 | # pseudo-simplistic-xml-parser |
136 | for (;;) { |
234 | for (;;) { |
137 | $text =~ /\G<([^>]+)>/gc or last; |
235 | $text =~ /\G<([^>]+)>/gc or last; |
138 | my $tag = $1; |
236 | my $tag = $1; |
139 | if ($tag =~ s/^\///) { |
237 | if ($tag =~ s/^\///) { |
… | |
… | |
141 | } else { |
239 | } else { |
142 | push @tag, $tag; |
240 | push @tag, $tag; |
143 | } |
241 | } |
144 | |
242 | |
145 | $text =~ /\G([^<]*)/gc or last; |
243 | $text =~ /\G([^<]*)/gc or last; |
146 | $self->{buffer}->insert_with_tags_by_name ($self->{buffer}->get_end_iter, util::xmlto $1, $tag[-1]) |
244 | $self->{buffer}->insert_with_tags_by_name ($self->{buffer}->get_iter_at_mark ($mark), util::xmlto $1, @tag) |
147 | if length $1; |
245 | if length $1; |
148 | } |
246 | } |
149 | |
247 | |
150 | $self->set_end if $at_end; |
248 | $self->set_end if $at_end; |
151 | } |
249 | } |
… | |
… | |
159 | $self->append_text ($text); |
257 | $self->append_text ($text); |
160 | |
258 | |
161 | $self->set_end if $at_end; |
259 | $self->set_end if $at_end; |
162 | } |
260 | } |
163 | |
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 | |
164 | 1; |
410 | 1; |
165 | |
411 | |