blib/lib/Gtk2/Ex/WYSIWYG.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 4 | 6 | 66.6 |
branch | n/a | ||
condition | n/a | ||
subroutine | 2 | 2 | 100.0 |
pod | n/a | ||
total | 6 | 8 | 75.0 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Gtk2::Ex::WYSIWYG; | ||||||
2 | |||||||
3 | 1 | 1 | 19822 | use strict; | |||
1 | 2 | ||||||
1 | 30 | ||||||
4 | 1 | 1 | 338 | use Gtk2; | |||
0 | |||||||
0 | |||||||
5 | use Gtk2::Pango; | ||||||
6 | use Glib::Object::Subclass | ||||||
7 | Gtk2::Table::, | ||||||
8 | signals => {}, | ||||||
9 | properties => [Glib::ParamSpec->uint('undo_stack', | ||||||
10 | 'Undo Stack Size', | ||||||
11 | ('The maximum size of the undo '. | ||||||
12 | 'stack. Zero implies no limit'), | ||||||
13 | 0, ~0, 0, | ||||||
14 | [qw/readable writable/]), | ||||||
15 | Glib::ParamSpec->boolean('flat_toolbar', | ||||||
16 | 'Flat Toolbar', | ||||||
17 | ('Whether the toolbar should be '. | ||||||
18 | 'flat (true) or double-height '. | ||||||
19 | '(false)'), | ||||||
20 | 0, [qw/readable writable/]), | ||||||
21 | Glib::ParamSpec->boolean('debug', | ||||||
22 | 'Show Debug Button', | ||||||
23 | ('Show or hide the Debug button'), | ||||||
24 | 0, [qw/readable writable/]), | ||||||
25 | Glib::ParamSpec->boolean('map-fill-to-left', | ||||||
26 | 'Map fill justification to left', | ||||||
27 | ('Map the fill justification tag '. | ||||||
28 | 'to the left justification tag '. | ||||||
29 | 'for older version of Gtk2 that '. | ||||||
30 | 'don\'t support it'), | ||||||
31 | 0, [qw/readable writable/]), | ||||||
32 | Glib::ParamSpec->boolean('check-spelling', | ||||||
33 | 'Check spelling', | ||||||
34 | ('Use Gtk2::Spell to allow spell '. | ||||||
35 | 'checking. You must have '. | ||||||
36 | 'Gtk2::Spell installed!'), | ||||||
37 | 0, [qw/readable writable/])]; | ||||||
38 | |||||||
39 | use constant UNDO_REMOVE_TAG => 0; | ||||||
40 | use constant UNDO_APPLY_TAG => 1; | ||||||
41 | use constant UNDO_INSERT_TEXT => 2; | ||||||
42 | use constant UNDO_DELETE_TEXT => 3; | ||||||
43 | |||||||
44 | =head1 NAME | ||||||
45 | |||||||
46 | Gtk2::Ex::WYSIWYG - A WYSIWYG editor ready to drop into a GUI. | ||||||
47 | |||||||
48 | =head1 VERSION | ||||||
49 | |||||||
50 | Version 0.02 | ||||||
51 | |||||||
52 | =cut | ||||||
53 | |||||||
54 | our $VERSION = 0.02; | ||||||
55 | |||||||
56 | =head1 DESCRIPTION | ||||||
57 | |||||||
58 | This module is a subclass of L |
||||||
59 | and a 'toolbar' to allow a user to edit and format text. It can serialise | ||||||
60 | to a plain text block and a tag stack, or to incomplete HTML (the output is | ||||||
61 | not a complete HTML document, but can be included inside one). It can also | ||||||
62 | 'deserialise' from this same data to easily allow content from one WYSIWYG to | ||||||
63 | be transfered to another - the more efficient of these is the text/tag stack, | ||||||
64 | however the HTML form can be more easily stored. | ||||||
65 | |||||||
66 | An undo/redo stack is also included, as well as a modification to the text | ||||||
67 | view's popup menu to allow the user to set the wrap mode with ease. | ||||||
68 | |||||||
69 | It should be noted that WYSIWYG emulates paragraphs by using \n\s*\n as a | ||||||
70 | paragraph separator. The leading newline in the sequence will belong to the | ||||||
71 | leading paragraph, and the rest to 'interparagraph space'. This has some | ||||||
72 | implications - interparagraph space honours vertical space (ie, extra newlines | ||||||
73 | will be rendered when exporting to HTML) but not horizontal space - any spaces | ||||||
74 | you put inside interparagraph space will be ignored, as will any font | ||||||
75 | formatting you apply. | ||||||
76 | |||||||
77 | It also means that should two paragraphs be joined by a user edit (either by | ||||||
78 | inserting non-whitespace or by deleteing whitespace) any paragraph-level | ||||||
79 | formatting applied to the paragraph that used to be before the interparagraph | ||||||
80 | space will be applied to any affected paragraphs after it. | ||||||
81 | |||||||
82 | See the TAGS section below for supported tags. | ||||||
83 | |||||||
84 | There are currently three 'sub-packages' contained within Gtk2::Ex::WYSIWYG as | ||||||
85 | well - Gtk2::Ex::WYSIWYG::HTML (for parsing and generating HTML from the view), | ||||||
86 | Gtk2::Ex::WYSIWYG::FormatMenu (a Gtk2::ComboBox replacement that shows | ||||||
87 | formatting in the option menu but not in the main widget) and | ||||||
88 | Gtk2::Ex::WYSIWYG::SizeMenu (a beefed up Gtk2::ComboBoxEntry with a few extra | ||||||
89 | features, specifically designed for the font size setting). | ||||||
90 | |||||||
91 | =head1 HIERARCHY | ||||||
92 | |||||||
93 | Glib::Object | ||||||
94 | +----Glib::InitiallyUnowned | ||||||
95 | +----Gtk2::Object | ||||||
96 | +----Gtk2::Widget | ||||||
97 | +----Gtk2::Container | ||||||
98 | +----Gtk2::Table | ||||||
99 | +---Gtk2::Ex::WYSIWYG | ||||||
100 | |||||||
101 | =head1 METHODS | ||||||
102 | |||||||
103 | =cut | ||||||
104 | |||||||
105 | #' emacs formatting.... | ||||||
106 | |||||||
107 | my %TAGS; # Tag definitions. See end of file for BEGIN filler | ||||||
108 | my %BUTTONS; # Button definitions. See end of file for BEGIN filler | ||||||
109 | |||||||
110 | # 'Public' methods | ||||||
111 | |||||||
112 | =head2 Gtk2::Ex::WYSIWYG->new() | ||||||
113 | |||||||
114 | Returns a new WYSIWYG instance. There are a few properties you can set, see | ||||||
115 | the PROPERTIES section below. | ||||||
116 | |||||||
117 | =cut | ||||||
118 | |||||||
119 | sub INIT_INSTANCE { | ||||||
120 | my $self = shift; | ||||||
121 | $self->_init_tooltips; | ||||||
122 | $self->_init_font_list if not defined $BUTTONS{Font}{Tags}; | ||||||
123 | $self->{FontSet} = 1; | ||||||
124 | $self->{SizeSet} = 1; | ||||||
125 | $self->{Active} = {}; | ||||||
126 | $self->{UndoStack} = []; | ||||||
127 | $self->{RedoStack} = []; | ||||||
128 | $self->{Record} = undef; | ||||||
129 | $self->_build_buttons; | ||||||
130 | $self->_build_toolbar; | ||||||
131 | $self->_build_text; | ||||||
132 | $self->_set_buttons_from_active; | ||||||
133 | $self->signal_connect(visibility_notify_event => | ||||||
134 | sub {$self->_on_visibility_notify}); | ||||||
135 | } | ||||||
136 | |||||||
137 | =head2 $wysiwyg->clear_undo() | ||||||
138 | |||||||
139 | Empties the undo and redo stacks. | ||||||
140 | |||||||
141 | =cut | ||||||
142 | |||||||
143 | sub clear_undo { | ||||||
144 | my $self = shift; | ||||||
145 | $self->{UndoStack} = []; | ||||||
146 | $self->{Record} = undef; | ||||||
147 | $self->_set_buttons_from_active; | ||||||
148 | } | ||||||
149 | |||||||
150 | =head2 $wysiwyg->undo() | ||||||
151 | |||||||
152 | Performs a single undo action. Does nothing if there is nothing to undo. | ||||||
153 | Undo actions are user-action based, so if a user made a change that actually | ||||||
154 | made multiple changes to the content, all those changes will be reversed at | ||||||
155 | once. | ||||||
156 | |||||||
157 | =cut | ||||||
158 | |||||||
159 | sub undo { | ||||||
160 | my $self = shift; | ||||||
161 | return if not scalar(@{$self->{UndoStack}}); | ||||||
162 | ++$self->{Undoing}; | ||||||
163 | my $undo = pop(@{$self->{UndoStack}}); | ||||||
164 | my $buf = $self->{Text}->get_buffer; | ||||||
165 | for my $step (reverse(@$undo)) { | ||||||
166 | my ($type, $from, $to, @args) = @$step; | ||||||
167 | if ($type == UNDO_INSERT_TEXT) { | ||||||
168 | # Remove text from $from to $to | ||||||
169 | $buf->delete($buf->get_iter_at_offset($from), | ||||||
170 | $buf->get_iter_at_offset($to)); | ||||||
171 | } elsif ($type == UNDO_DELETE_TEXT) { | ||||||
172 | # Reinsert text at $from | ||||||
173 | $buf->insert($buf->get_iter_at_offset($from), $args[0]); | ||||||
174 | } elsif ($type == UNDO_APPLY_TAG) { | ||||||
175 | $buf->remove_tag($args[0], $buf->get_iter_at_offset($from), | ||||||
176 | $buf->get_iter_at_offset($to)); | ||||||
177 | } elsif ($type == UNDO_REMOVE_TAG) { | ||||||
178 | $buf->apply_tag($args[0], $buf->get_iter_at_offset($from), | ||||||
179 | $buf->get_iter_at_offset($to)); | ||||||
180 | } | ||||||
181 | } | ||||||
182 | push @{$self->{RedoStack}}, $undo; | ||||||
183 | --$self->{Undoing}; | ||||||
184 | $self->_set_active_from_text; | ||||||
185 | $self->_set_buttons_from_active; | ||||||
186 | return 0; | ||||||
187 | } | ||||||
188 | |||||||
189 | =head2 $wysiwyg->redo() | ||||||
190 | |||||||
191 | Performs a single redo action. Does nothing if there is nothing to redo. | ||||||
192 | Undo actions are user-action based, so if a user made a change that actually | ||||||
193 | made multiple changes to the content, all those changes will be reapplied at | ||||||
194 | once. | ||||||
195 | |||||||
196 | =cut | ||||||
197 | |||||||
198 | sub redo { | ||||||
199 | my $self = shift; | ||||||
200 | return if not scalar(@{$self->{RedoStack}}); | ||||||
201 | ++$self->{Undoing}; | ||||||
202 | my $redo = pop(@{$self->{RedoStack}}); | ||||||
203 | my $buf = $self->{Text}->get_buffer; | ||||||
204 | for my $step (@$redo) { | ||||||
205 | my ($type, $from, $to, @args) = @$step; | ||||||
206 | if ($type == UNDO_INSERT_TEXT) { | ||||||
207 | $buf->insert($buf->get_iter_at_offset($from), $args[0]); | ||||||
208 | } elsif ($type == UNDO_DELETE_TEXT) { | ||||||
209 | $buf->delete($buf->get_iter_at_offset($from), | ||||||
210 | $buf->get_iter_at_offset($to)); | ||||||
211 | } elsif ($type == UNDO_APPLY_TAG) { | ||||||
212 | $buf->apply_tag($args[0], $buf->get_iter_at_offset($from), | ||||||
213 | $buf->get_iter_at_offset($to)); | ||||||
214 | } elsif ($type == UNDO_REMOVE_TAG) { | ||||||
215 | $buf->remove_tag($args[0], $buf->get_iter_at_offset($from), | ||||||
216 | $buf->get_iter_at_offset($to)); | ||||||
217 | } | ||||||
218 | } | ||||||
219 | push @{$self->{UndoStack}}, $redo; | ||||||
220 | --$self->{Undoing}; | ||||||
221 | $self->_set_active_from_text; | ||||||
222 | $self->_set_buttons_from_active; | ||||||
223 | return 0; | ||||||
224 | } | ||||||
225 | |||||||
226 | =head2 $textview = $wysiwyg->get_text() | ||||||
227 | |||||||
228 | Returns the Gtk2::TextView widget that forms the main body of the WYSIWYG | ||||||
229 | mega-widget. Please be careful with it - making direct modifications may | ||||||
230 | seriously confuse the serialisation/deserialisation methods. | ||||||
231 | |||||||
232 | =cut | ||||||
233 | |||||||
234 | sub get_text { $_[0]->{Text} } | ||||||
235 | |||||||
236 | =head2 $textbuffer = $wysiwyg->get_buffer() | ||||||
237 | |||||||
238 | Returns the Gtk2::TextBuffer widget within the WYSIWYG mega-widget. Toy with | ||||||
239 | this at your peril. | ||||||
240 | |||||||
241 | =cut | ||||||
242 | |||||||
243 | sub get_buffer { $_[0]->{Text}->get_buffer } | ||||||
244 | |||||||
245 | =head2 ($text, @tags) = $wysiwyg->serialise() | ||||||
246 | |||||||
247 | The more efficient of the (currently) two serialisation methods, serialise | ||||||
248 | will return both the raw text and a sequence of tags that when applied to the | ||||||
249 | text will render the original look. | ||||||
250 | |||||||
251 | Tags are hashrefs with keys of 'Start' (the index to start applying the tag), | ||||||
252 | 'End' (the index to stop applying the tag) and 'Tags' (a hashref of key value | ||||||
253 | pairs containing the actual tag information). They are ordered by the Start | ||||||
254 | key, and they do NOT overlap (ie, one tag's range is never inside the range of | ||||||
255 | another tag). | ||||||
256 | |||||||
257 | Tags include more than just the tags applied by the user - three other tags are | ||||||
258 | also added (and take precedence over user tags) - a 'br' tag (for | ||||||
259 | intra-paragraph newlines), a 'p' tag (to specify interparagraph space) and | ||||||
260 | a 'ws' tag (to tag multiple-character whitespace strings). These are mainly | ||||||
261 | used for conversion to HTML. | ||||||
262 | |||||||
263 | =cut | ||||||
264 | |||||||
265 | #' emacs formatting | ||||||
266 | |||||||
267 | sub serialise { | ||||||
268 | my $self = shift; | ||||||
269 | my @user = $self->_get_user_tags; | ||||||
270 | my $buf = $self->{Text}->get_buffer; | ||||||
271 | return ($buf->get_text($buf->get_bounds, 0), @user); | ||||||
272 | } | ||||||
273 | |||||||
274 | =head2 $wysiwyg->deserialise($txt, @tags) | ||||||
275 | |||||||
276 | The inverse of serialise. Note that this also clears the undo and redo stacks. | ||||||
277 | |||||||
278 | =cut | ||||||
279 | |||||||
280 | sub deserialise { | ||||||
281 | my $self = shift; | ||||||
282 | my ($txt, @tags) = @_; | ||||||
283 | # This wipes undo! | ||||||
284 | $self->{UndoStack} = []; | ||||||
285 | $self->{RedoStack} = []; | ||||||
286 | $self->{Record} = undef; | ||||||
287 | ++$self->{Undoing}; | ||||||
288 | my $buf = $self->{Text}->get_buffer; | ||||||
289 | { | ||||||
290 | my @rem; | ||||||
291 | my $tt = $buf->get_tag_table; | ||||||
292 | # Remove all of my tags? | ||||||
293 | $tt->foreach(sub { | ||||||
294 | push @rem, $_[0] if $self->_is_my_tag($_[0]) | ||||||
295 | }); | ||||||
296 | for my $rem (@rem) { | ||||||
297 | $tt->remove($rem); | ||||||
298 | } | ||||||
299 | $self->{LinkID} = 0; | ||||||
300 | } | ||||||
301 | $buf->delete($buf->get_bounds); | ||||||
302 | $buf->insert($buf->get_start_iter, $txt); | ||||||
303 | $txt = undef; | ||||||
304 | for my $tag (@tags) { | ||||||
305 | # Start, End and Tags (name => val?) | ||||||
306 | my $s = $buf->get_iter_at_offset($tag->{Start}); | ||||||
307 | my $e = $buf->get_iter_at_offset($tag->{End}); | ||||||
308 | my $size = 10; | ||||||
309 | $size = $tag->{Tags}{size} if exists $tag->{Tags}{size}; | ||||||
310 | my $hscale = 1; | ||||||
311 | for my $tname (keys %{$tag->{Tags}}) { | ||||||
312 | next if $tname !~ /^h[1-5]\z/; | ||||||
313 | $hscale = $TAGS{$tname}{Look}{scale}; | ||||||
314 | last; | ||||||
315 | } | ||||||
316 | $hscale = 1 if not $hscale; | ||||||
317 | for my $tname (keys %{$tag->{Tags}}) { | ||||||
318 | my $val = $tag->{Tags}{$tname}; | ||||||
319 | my $t; | ||||||
320 | if ($tname eq 'link') { | ||||||
321 | $t = $self->_create_link($val); | ||||||
322 | } elsif ($tname eq 'font') { | ||||||
323 | $t = $self->_create_tag($self->_full_tag_name('font', $val->[0]), | ||||||
324 | family => $val->[0]); | ||||||
325 | } elsif ($tname eq 'size') { | ||||||
326 | $t = $self->_create_tag($self->_full_tag_name('size', $val->[0]), | ||||||
327 | size => $val->[0] * 1024); | ||||||
328 | } elsif ($tname eq 'superscript' or $tname eq 'subscript') { | ||||||
329 | my ($sz, $sc) = ($size, $hscale); | ||||||
330 | if (defined($val)) { | ||||||
331 | $sz = $val->[0] if defined($val->[0]); | ||||||
332 | $sc = $val->[1] if defined($val->[1]); | ||||||
333 | } | ||||||
334 | $t = $self->_create_sub_super_tag($tname, $sz, $sc); | ||||||
335 | } elsif ($tname eq 'indent') { | ||||||
336 | $t = $self->_create_tag($self->_full_tag_name('indent', $val->[0]), | ||||||
337 | 'left-margin' => 32 * ($val->[0] + 1)); | ||||||
338 | } elsif (not defined $val and | ||||||
339 | exists $TAGS{$tname} and exists $TAGS{$tname}{Look}) { | ||||||
340 | $t = $self->_create_tag($self->_full_tag_name($tname), | ||||||
341 | %{$TAGS{$tname}{Look}}); | ||||||
342 | } | ||||||
343 | $self->_apply_tag($t, $s, $e) if defined $t; | ||||||
344 | } | ||||||
345 | } | ||||||
346 | --$self->{Undoing}; | ||||||
347 | $self->_set_active_from_text; | ||||||
348 | $self->_set_buttons_from_active; | ||||||
349 | } | ||||||
350 | |||||||
351 | =head2 $text = $wysiwyg->get_html() | ||||||
352 | |||||||
353 | Outputs the contents of the WYSIWYG as HTML. This can also be used as a less | ||||||
354 | efficient but more storable serialisation method as the WYSIWYG can re-parse | ||||||
355 | the output HTML and display it. | ||||||
356 | |||||||
357 | Note that the output HTML is incomplete - only the formatting markup is | ||||||
358 | included, but it would be trivial to wrap the appropriate tags around it. | ||||||
359 | |||||||
360 | Font sizes are a little tricky, so WYSIWYG converts sizes to em values | ||||||
361 | (assuming size 16 is 1 em). | ||||||
362 | |||||||
363 | Remember that as-is tags are not 'html-cleaned' (that's the point - so you can | ||||||
364 | insert HTML tags that WYSIWYG itself doesn't support), so be careful! | ||||||
365 | |||||||
366 | =cut | ||||||
367 | |||||||
368 | sub get_html { | ||||||
369 | my $self = shift; | ||||||
370 | my @user = $self->_get_user_tags; | ||||||
371 | my @auto = $self->_get_auto_tags; | ||||||
372 | my @tags = $self->_merge_tags(\@user, \@auto); | ||||||
373 | my $buf = $self->{Text}->get_buffer; | ||||||
374 | return Gtk2::Ex::WYSIWYG::HTML->generate($buf, @tags); | ||||||
375 | } | ||||||
376 | |||||||
377 | =head2 $wysiwyg->set_html($text) | ||||||
378 | |||||||
379 | The inverse of get_html, this takes HTML text and attempts to parse it back | ||||||
380 | into the WYSIWYG. | ||||||
381 | |||||||
382 | While this is primarily designed to take text created with get_html, it can | ||||||
383 | handle being given arbitrary HTML. Any HTML tags it doesn't understand it | ||||||
384 | will insert tagged as 'as-is', so that a later call to get_html should | ||||||
385 | return something very similar to what was given to set_html. | ||||||
386 | |||||||
387 | =cut | ||||||
388 | |||||||
389 | #'emacs formatting | ||||||
390 | |||||||
391 | sub set_html { | ||||||
392 | my $self = shift; | ||||||
393 | my ($html) = @_; | ||||||
394 | # This wipes undo! | ||||||
395 | $self->{UndoStack} = []; | ||||||
396 | $self->{RedoStack} = []; | ||||||
397 | $self->{Record} = undef; | ||||||
398 | ++$self->{Undoing}; | ||||||
399 | my ($txt, @tags) = Gtk2::Ex::WYSIWYG::HTML->parse($html); | ||||||
400 | --$self->{Undoing}; | ||||||
401 | $self->deserialise($txt, @tags); | ||||||
402 | } | ||||||
403 | |||||||
404 | =head2 $wysiwyg->debug() | ||||||
405 | |||||||
406 | This function is what is called by the special 'debug' button (which appears | ||||||
407 | if you set the debug property to true). By default it simply prints | ||||||
408 | "DEBUG\n" to the screen, but you can override it to do whatever you like. | ||||||
409 | |||||||
410 | Two examples are included in the function - the first tests the serialisation | ||||||
411 | by serialising the current text and then deserialising that data back into the | ||||||
412 | WYSIWYG, and the second does the same but for the HTML serialisation. | ||||||
413 | |||||||
414 | =cut | ||||||
415 | |||||||
416 | #'emacs formatting | ||||||
417 | |||||||
418 | sub debug { | ||||||
419 | my $self = shift; | ||||||
420 | print "DEBUG!\n"; | ||||||
421 | # Check serialisation | ||||||
422 | # $self->deserialise($self->serialise); | ||||||
423 | |||||||
424 | # Check serialisation via html | ||||||
425 | # $self->set_html($self->get_html); | ||||||
426 | return 0; | ||||||
427 | } | ||||||
428 | |||||||
429 | # That's it for 'public' methods. | ||||||
430 | |||||||
431 | =head1 PROPERTIES | ||||||
432 | |||||||
433 | =head2 'undo-stack' (Glib::UInt : readable / writable) | ||||||
434 | |||||||
435 | The number of items allowed on the undo and redo stacks. A value of zero | ||||||
436 | indicates no limit, which is the default. | ||||||
437 | |||||||
438 | =head2 'check-spelling' (Glib::Boolean : readable/writable) | ||||||
439 | |||||||
440 | If this is turned on (and you have Gtk2::Spell installed), WYSIWYG will attach | ||||||
441 | a Gtk2::Spell instance to its text view. | ||||||
442 | |||||||
443 | =head2 'flat-toolbar' (Glib::Boolean : readable/writable) | ||||||
444 | |||||||
445 | The tool bar can be rendered either as 'fat' (two lines of buttons with named | ||||||
446 | groups) or 'flat' (one line of buttons). If flat-toolbar is set to true the | ||||||
447 | latter will be used, otherwise the former will be. The change will be mirrored | ||||||
448 | in the widget immediately. The default toolbar is the 'fat' version. | ||||||
449 | |||||||
450 | =head2 'map-fill-to-left' (Glib::Boolean : readable/writable) | ||||||
451 | |||||||
452 | Old versions of Gtk2 don't support the fill justification method, and will | ||||||
453 | complain loudly if you try to use it. If you're using such a version, set | ||||||
454 | this property to true to make WYSIWYG use the left justification tag instead. | ||||||
455 | |||||||
456 | This won't affect how the WYSIWYG outputs justification data - just how it | ||||||
457 | displays it. | ||||||
458 | |||||||
459 | =head2 'debug' (Glib::Boolean : readable/writable) | ||||||
460 | |||||||
461 | When set to true, this activates the 'debug' button on the toolbar. This button | ||||||
462 | will trigger the WYSIWYG's debug method - you'll probably want to override that | ||||||
463 | to do something useful. | ||||||
464 | |||||||
465 | =cut | ||||||
466 | |||||||
467 | # Move properties into their own parent key | ||||||
468 | sub GET_PROPERTY { | ||||||
469 | my $self = shift; | ||||||
470 | my ($pspec) = @_; | ||||||
471 | return ($self->{Properties}{$pspec->get_name} || $pspec->get_default_value); | ||||||
472 | } | ||||||
473 | |||||||
474 | sub SET_PROPERTY { | ||||||
475 | my $self = shift; | ||||||
476 | my ($pspec, $newval) = @_; | ||||||
477 | my $name = $pspec->get_name; | ||||||
478 | my $old = $self->get_property($name); | ||||||
479 | if ($name eq 'flat_toolbar' and | ||||||
480 | $newval != $self->get_property('flat_toolbar')) { | ||||||
481 | $self->{Properties}{flat_toolbar} = $newval; | ||||||
482 | $self->_build_buttons; # Shouldn't be a problem if done again | ||||||
483 | $self->_build_toolbar; | ||||||
484 | } elsif ($name eq 'debug' and | ||||||
485 | $newval != $self->get_property('debug')) { | ||||||
486 | $self->{Properties}{debug} = $newval; | ||||||
487 | if ($newval) { | ||||||
488 | if (not defined $self->{Buttons}{DUMP}) { | ||||||
489 | $self->{Buttons}{DUMP} = Gtk2::Button->new; | ||||||
490 | $self->{Buttons}{DUMP}-> | ||||||
491 | set_image(Gtk2::Image->new_from_stock('gtk-dialog-warning', | ||||||
492 | 'button')); | ||||||
493 | $self->{Buttons}{DUMP}->signal_connect('clicked', sub{$self->debug}); | ||||||
494 | } | ||||||
495 | $self->_build_buttons; | ||||||
496 | $self->_build_toolbar; | ||||||
497 | } | ||||||
498 | } elsif ($name eq 'map_fill_to_left' and | ||||||
499 | $newval != $self->get_property('map-fill-to-left')) { | ||||||
500 | $self->{Properties}{map_fill_to_left} = $newval; | ||||||
501 | if (defined $self->{Text}) { | ||||||
502 | my $tag = $self->{Text}->get_buffer->get_tag_table->lookup('fill'); | ||||||
503 | die("Gtk2::Ex::WYSIWYG tag naming conflict for fill - " . | ||||||
504 | "tag name already in use!") if not $self->_is_my_tag($tag); | ||||||
505 | $tag->set_property(justification => ($newval ? 'left' : 'fill')) | ||||||
506 | if defined $tag and $self->_is_my_tag($tag); | ||||||
507 | } | ||||||
508 | } elsif ($name eq 'check_spelling' and | ||||||
509 | $newval != $self->get_property('check-spelling')) { | ||||||
510 | $self->{Properties}{check_spelling} = $newval; | ||||||
511 | if ($newval) { | ||||||
512 | eval {require Gtk2::Spell}; | ||||||
513 | if ($@) { | ||||||
514 | warn("Gtk2::Spell does not appear to be installed!"); | ||||||
515 | return; | ||||||
516 | } | ||||||
517 | if (not defined $self->{GtkSpell} and defined($self->{Text})) { | ||||||
518 | $self->{GtkSpell} = Gtk2::Spell->new_attach($self->{Text}); | ||||||
519 | $self->{GtkSpell}->recheck_all; | ||||||
520 | } | ||||||
521 | } elsif (defined($self->{GtkSpell})) { | ||||||
522 | $self->{GtkSpell}->detach; | ||||||
523 | $self->{GtkSpell} = undef; | ||||||
524 | } | ||||||
525 | } else { | ||||||
526 | $self->{Properties}{$name} = $newval; | ||||||
527 | } | ||||||
528 | } | ||||||
529 | |||||||
530 | =head1 TAGS | ||||||
531 | |||||||
532 | There are two classes of tags available in the WYSIWYG - font tags and | ||||||
533 | paragraph tags. | ||||||
534 | |||||||
535 | =head2 Font Tags | ||||||
536 | |||||||
537 | Font tags are applied to arbitrary lengths of text, and only affect those | ||||||
538 | lengths of text. | ||||||
539 | |||||||
540 | The following font tags are pre-defined: | ||||||
541 | |||||||
542 | =head3 font | ||||||
543 | |||||||
544 | =head3 size | ||||||
545 | |||||||
546 | =head3 bold | ||||||
547 | |||||||
548 | =head3 italic | ||||||
549 | |||||||
550 | =head3 underline | ||||||
551 | |||||||
552 | =head3 strikethrough | ||||||
553 | |||||||
554 | =head3 superscript | ||||||
555 | |||||||
556 | Cannot be applied to text at the same time as subscript. | ||||||
557 | |||||||
558 | =head3 subscript | ||||||
559 | |||||||
560 | Cannot be applied to text at the same time as superscript. | ||||||
561 | |||||||
562 | =head3 link | ||||||
563 | |||||||
564 | =head3 pre | ||||||
565 | |||||||
566 | Preformatted text, like the HTML tag. | ||||||
567 | |||||||
568 | =head3 asis | ||||||
569 | |||||||
570 | A special tag that allows you to enter 'code' that the WYSIWYG would otherwise | ||||||
571 | not be able to understand as formatting. All other font tags are removed from | ||||||
572 | text marked as 'as-is'. | ||||||
573 | |||||||
574 | =head2 Paragraph Tags | ||||||
575 | |||||||
576 | Paragraph tags apply to a whole paragraph, and cannot be applied to only part | ||||||
577 | of a paragraph. | ||||||
578 | |||||||
579 | The following paragraph tags are predefined: | ||||||
580 | |||||||
581 | =head3 h1 | ||||||
582 | |||||||
583 | Heading 1 - cannot be used in the same paragraph as other Heading tags. | ||||||
584 | |||||||
585 | =head3 h2 | ||||||
586 | |||||||
587 | Heading 2 - cannot be used in the same paragraph as other Heading tags. | ||||||
588 | |||||||
589 | =head3 h3 | ||||||
590 | |||||||
591 | Heading 3 - cannot be used in the same paragraph as other Heading tags. | ||||||
592 | |||||||
593 | =head3 h4 | ||||||
594 | |||||||
595 | Heading 4 - cannot be used in the same paragraph as other Heading tags. | ||||||
596 | |||||||
597 | =head3 h5 | ||||||
598 | |||||||
599 | Heading 5 - cannot be used in the same paragraph as other Heading tags. | ||||||
600 | |||||||
601 | =head3 left | ||||||
602 | |||||||
603 | Left justification - cannot be used in the same paragraph as other | ||||||
604 | Justification tags. | ||||||
605 | |||||||
606 | =head3 center | ||||||
607 | |||||||
608 | Center justification - cannot be used in the same paragraph as other | ||||||
609 | Justification tags. | ||||||
610 | |||||||
611 | =head3 right | ||||||
612 | |||||||
613 | Right justification - cannot be used in the same paragraph as other | ||||||
614 | Justification tags. | ||||||
615 | |||||||
616 | =head3 fill | ||||||
617 | |||||||
618 | Fill justification - cannot be used in the same paragraph as other | ||||||
619 | Justification tags. See the 'map-fill-to-left' property for older versions of | ||||||
620 | Gtk2 that do not support fill justification properly. | ||||||
621 | |||||||
622 | =head3 indent | ||||||
623 | |||||||
624 | The size of the left margin (or the right for right justified paragraphs). | ||||||
625 | |||||||
626 | =head1 AUTHOR | ||||||
627 | |||||||
628 | Matthew Braid, C<< |
||||||
629 | |||||||
630 | =head1 TODO | ||||||
631 | |||||||
632 | =over 4 | ||||||
633 | |||||||
634 | =item * Separate the toolbar from the text view | ||||||
635 | |||||||
636 | =item * Find some way to support bulleted/numbered lists | ||||||
637 | |||||||
638 | =back | ||||||
639 | |||||||
640 | =head1 BUGS | ||||||
641 | |||||||
642 | Please report any bugs or feature requests to C |
||||||
643 | the web interface at L |
||||||
644 | automatically be notified of progress on your bug as I make changes. | ||||||
645 | |||||||
646 | =head1 SUPPORT | ||||||
647 | |||||||
648 | You can find documentation for this module with the perldoc command. | ||||||
649 | |||||||
650 | perldoc Gtk2::Ex::WYSIWYG | ||||||
651 | |||||||
652 | |||||||
653 | You can also look for information at: | ||||||
654 | |||||||
655 | =over 4 | ||||||
656 | |||||||
657 | =item * RT: CPAN's request tracker | ||||||
658 | |||||||
659 | L |
||||||
660 | |||||||
661 | =item * AnnoCPAN: Annotated CPAN documentation | ||||||
662 | |||||||
663 | L |
||||||
664 | |||||||
665 | =item * CPAN Ratings | ||||||
666 | |||||||
667 | L |
||||||
668 | |||||||
669 | =item * Search CPAN | ||||||
670 | |||||||
671 | L |
||||||
672 | |||||||
673 | =back | ||||||
674 | |||||||
675 | =head1 LICENSE AND COPYRIGHT | ||||||
676 | |||||||
677 | Copyright 2010 Matthew Braid. | ||||||
678 | |||||||
679 | This program is free software; you can redistribute it and/or modify it | ||||||
680 | under the terms of either: the GNU General Public License as published | ||||||
681 | by the Free Software Foundation; or the Artistic License. | ||||||
682 | |||||||
683 | See http://dev.perl.org/licenses/ for more information. | ||||||
684 | |||||||
685 | =cut | ||||||
686 | |||||||
687 | ############################################################################ | ||||||
688 | # Builder functions - used to create class and instance widgets as necessary | ||||||
689 | ############################################################################ | ||||||
690 | |||||||
691 | ######### | ||||||
692 | # Create the tooltips - both the 'standard' tooltips widget for hovering | ||||||
693 | # over buttons and a 'fake' one for hovering over links | ||||||
694 | ######### | ||||||
695 | BEGIN { | ||||||
696 | my ($TT, $TT_L); # Fake tooltips and label for same | ||||||
697 | my $TOOLTIPS; # 'Real' tooltips widget for buttons | ||||||
698 | |||||||
699 | sub _init_tooltips { | ||||||
700 | my $self = shift; | ||||||
701 | return if defined $TOOLTIPS; | ||||||
702 | $TOOLTIPS = Gtk2::Tooltips->new; # Class wide. Would be nice if there was a | ||||||
703 | # way of determining if a tooltip widget | ||||||
704 | # was already created and use that | ||||||
705 | $TT = Gtk2::Window->new('popup'); # The 'fake' link tooltip window | ||||||
706 | $TT_L = Gtk2::Label->new; | ||||||
707 | $TT_L->set_padding(4, 4); | ||||||
708 | $TT->set_resizable(0); | ||||||
709 | $TT->set_decorated(0); | ||||||
710 | $TT->set_position('mouse'); # We modify this on popup | ||||||
711 | # Would be good to get the current theme colour - on ubuntu this works, but | ||||||
712 | # using blackbox on freebsd results in a colour that is 'too yellow' | ||||||
713 | $TT->modify_bg('normal', | ||||||
714 | Gtk2::Gdk::Color->new(245 << 8, 245 << 8, 181 << 8)); | ||||||
715 | my $frame = Gtk2::Frame->new; | ||||||
716 | $frame->set_shadow_type('etched-in'); | ||||||
717 | $frame->add($TT_L); | ||||||
718 | $TT->add($frame); | ||||||
719 | } | ||||||
720 | |||||||
721 | sub _tooltip_text { | ||||||
722 | my $self = shift; | ||||||
723 | my ($txt) = @_; | ||||||
724 | $TT_L->set_text($txt); | ||||||
725 | } | ||||||
726 | |||||||
727 | sub _tooltip_show { | ||||||
728 | my $self = shift; | ||||||
729 | my ($x, $y) = @_; | ||||||
730 | $TT->show_all; | ||||||
731 | my ($thisx, $thisy) = $TT->window->get_origin; | ||||||
732 | $TT->move($thisx + 20, $thisy + 20); | ||||||
733 | } | ||||||
734 | |||||||
735 | sub _tooltip_hide { $TT->hide } | ||||||
736 | |||||||
737 | ########## | ||||||
738 | # _build_buttons - on an instance creation, build the buttons for the toolbar | ||||||
739 | # at the top. Uses the %BUTTONS and %TAGS hashes (see below) | ||||||
740 | # In this begin block to access $TOOLTIPS | ||||||
741 | ########## | ||||||
742 | sub _build_buttons { | ||||||
743 | my $self = shift; | ||||||
744 | for my $bname (keys %BUTTONS) { | ||||||
745 | return if defined($self->{Buttons}{$bname}); | ||||||
746 | if ($BUTTONS{$bname}{Type} eq 'toggle') { | ||||||
747 | $self->{Buttons}{$bname} = Gtk2::ToggleButton->new; | ||||||
748 | $self->{Buttons}{$bname}->set_active(1) | ||||||
749 | if $BUTTONS{$bname}{On}; | ||||||
750 | $TOOLTIPS->set_tip($self->{Buttons}{$bname}, | ||||||
751 | $BUTTONS{$bname}{TipText}); | ||||||
752 | if ($TAGS{$BUTTONS{$bname}{Tag}}{Multi}) { | ||||||
753 | $self->{Buttons}{$bname}-> | ||||||
754 | signal_connect('toggled', | ||||||
755 | sub {$self->_on_multi_toggle_change($bname)}); | ||||||
756 | } else { | ||||||
757 | $self->{Buttons}{$bname}-> | ||||||
758 | signal_connect('toggled', sub {$self->_on_toggle_change($bname)}); | ||||||
759 | } | ||||||
760 | } elsif ($BUTTONS{$bname}{Type} eq 'button') { | ||||||
761 | $self->{Buttons}{$bname} = Gtk2::Button->new; | ||||||
762 | $TOOLTIPS->set_tip($self->{Buttons}{$bname}, | ||||||
763 | $BUTTONS{$bname}{TipText}); | ||||||
764 | $self->{Buttons}{$bname}-> | ||||||
765 | signal_connect('clicked', sub {$self->_on_button_click($bname)}); | ||||||
766 | } elsif ($BUTTONS{$bname}{Type} eq 'menu') { | ||||||
767 | $self->{Buttons}{$bname} = Gtk2::Ex::WYSIWYG::FormatMenu->new; | ||||||
768 | $self->{Buttons}{$bname}->set_tool_tip($TOOLTIPS); | ||||||
769 | $self->{Buttons}{$bname}-> | ||||||
770 | signal_connect(format_selected => | ||||||
771 | sub {$self->_on_menu_change($bname, @_)}); | ||||||
772 | $self->{Buttons}{$bname}-> | ||||||
773 | set_options(map({[$_->[1], $_->[0], | ||||||
774 | ((exists($TAGS{$_->[0]}) and | ||||||
775 | exists($TAGS{$_->[0]}{Look})) | ||||||
776 | ? $TAGS{$_->[0]}{Look} | ||||||
777 | : undef)]} | ||||||
778 | @{$BUTTONS{$bname}{Tags}})); | ||||||
779 | $self->{Buttons}{$bname}->set_ellipsize('end'); | ||||||
780 | $self->{Buttons}{$bname}->set_default($BUTTONS{$bname}{Default}); | ||||||
781 | $self->{Buttons}{$bname}->set_text($BUTTONS{$bname}{Default}); | ||||||
782 | } elsif ($BUTTONS{$bname}{Type} eq 'font') { | ||||||
783 | $self->{Buttons}{$bname} = Gtk2::Ex::WYSIWYG::FormatMenu->new; | ||||||
784 | $self->{Buttons}{$bname}->set_tool_tip($TOOLTIPS); | ||||||
785 | $self->{Buttons}{$bname}-> | ||||||
786 | signal_connect(format_selected => | ||||||
787 | sub {$self->_on_font_change($bname, @_)}); | ||||||
788 | $self->{Buttons}{$bname}-> | ||||||
789 | set_options(map({[$_, $_, {family => $_}]} | ||||||
790 | @{$BUTTONS{$bname}{Tags}})); | ||||||
791 | $self->{Buttons}{$bname}->set_ellipsize('end'); | ||||||
792 | $self->{Buttons}{$bname}->set_default($BUTTONS{$bname}{Default}); | ||||||
793 | $self->{Buttons}{$bname}->set_text($BUTTONS{$bname}{Default}); | ||||||
794 | } elsif ($BUTTONS{$bname}{Type} eq 'size') { | ||||||
795 | $self->{Buttons}{$bname} = Gtk2::Ex::WYSIWYG::SizeMenu->new; | ||||||
796 | $self->{Buttons}{$bname}->set_value($BUTTONS{$bname}{Default}); | ||||||
797 | $self->{Buttons}{$bname}-> | ||||||
798 | signal_connect(size_selected => sub {$self->_on_size_change($bname, | ||||||
799 | @_)}); | ||||||
800 | } else { | ||||||
801 | next; | ||||||
802 | } | ||||||
803 | # Eeek! This won't work if the button has both an image and text! | ||||||
804 | $self->{Buttons}{$bname}-> | ||||||
805 | set_image(Gtk2::Image->new_from_stock($BUTTONS{$bname}{Image}, | ||||||
806 | 'button')) | ||||||
807 | if exists $BUTTONS{$bname}{Image}; | ||||||
808 | $self->{Buttons}{$bname}->set_label($BUTTONS{$bname}{Label}) | ||||||
809 | if exists $BUTTONS{$bname}{Label}; | ||||||
810 | $self->{Buttons}{$bname}->set_focus_on_click(0); | ||||||
811 | } | ||||||
812 | } | ||||||
813 | } | ||||||
814 | |||||||
815 | sub _clear_toolbar { | ||||||
816 | my $self = shift; | ||||||
817 | return if not defined $self->{Toolbar}; | ||||||
818 | for my $child ($self->{Toolbar}->get_children) { | ||||||
819 | $self->_clear_toolbar_part($child) | ||||||
820 | if $child->isa('Gtk2::Box') or $child->isa('Gtk2::Frame'); | ||||||
821 | $self->{Toolbar}->remove($child); | ||||||
822 | } | ||||||
823 | $self->remove($self->{Toolbar}); | ||||||
824 | } | ||||||
825 | |||||||
826 | sub _clear_toolbar_part { | ||||||
827 | my $self = shift; | ||||||
828 | my ($part) = @_; | ||||||
829 | for my $child ($part->get_children) { | ||||||
830 | if ($child->isa('Gtk2::Box') or $child->isa('Gtk2::Frame')) { | ||||||
831 | $self->_clear_toolbar_part($child); | ||||||
832 | } | ||||||
833 | $part->remove($child); | ||||||
834 | } | ||||||
835 | } | ||||||
836 | |||||||
837 | ########## | ||||||
838 | # _build_toolbar - once the buttons are built, pack them into a nice format as | ||||||
839 | # the toolbar | ||||||
840 | ########## | ||||||
841 | sub _build_toolbar { | ||||||
842 | my $self = shift; | ||||||
843 | $self->_clear_toolbar; | ||||||
844 | if ($self->{Properties}{flat_toolbar}) { | ||||||
845 | $self->_build_flat_toolbar; | ||||||
846 | } else { | ||||||
847 | $self->_build_fat_toolbar; | ||||||
848 | } | ||||||
849 | } | ||||||
850 | |||||||
851 | sub _build_flat_toolbar { | ||||||
852 | my $self = shift; | ||||||
853 | # +--------------------------------------------------------------... | ||||||
854 | # |+----------------------------FONT----------------------------+... | ||||||
855 | # || FONTV SIZEV | SZ+ SZ- | B I U S sub SUP CASE | BG FG | CLR |... | ||||||
856 | # |+------------------------------------------------------------+... | ||||||
857 | # +--------------------------------------------------------------... | ||||||
858 | |||||||
859 | # ...-------------------------------------+ | ||||||
860 | # ...-------------PARAGRAPH---------++---+| | ||||||
861 | # ...I- I+ | L C R F | HEADING TYPE ||U R|| | ||||||
862 | # ...-------------------------------++---+| | ||||||
863 | # ...-------------------------------------+ | ||||||
864 | $self->{Toolbar} = Gtk2::HBox->new(0, 0) | ||||||
865 | if not defined $self->{Toolbar}; | ||||||
866 | |||||||
867 | # FONT BLOCK | ||||||
868 | my $frame = Gtk2::Frame->new(); | ||||||
869 | $frame->set_shadow_type('etched-in'); | ||||||
870 | $frame->set_border_width(2); | ||||||
871 | $self->{Toolbar}->pack_start($frame, 0, 0, 2); | ||||||
872 | my $hb2 = Gtk2::HBox->new(0, 0); | ||||||
873 | $frame->add($hb2); | ||||||
874 | $self->{Buttons}{Font}->set_width_chars(16); | ||||||
875 | $hb2->pack_start($self->{Buttons}{Font}, 1, 1, 0); | ||||||
876 | $hb2->pack_start($self->{Buttons}{Size}, 0, 0, 0); | ||||||
877 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
878 | $hb2->pack_start($self->{Buttons}{SizeUp}, 0, 0, 0); | ||||||
879 | $hb2->pack_start($self->{Buttons}{SizeDown}, 0, 0, 0); | ||||||
880 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
881 | $hb2->pack_start($self->{Buttons}{Bold}, 0, 0, 0); | ||||||
882 | $hb2->pack_start($self->{Buttons}{Italic}, 0, 0, 0); | ||||||
883 | $hb2->pack_start($self->{Buttons}{Underline}, 0, 0, 0); | ||||||
884 | $hb2->pack_start($self->{Buttons}{Strike}, 0, 0, 0); | ||||||
885 | $hb2->pack_start($self->{Buttons}{Sub}, 0, 0, 0); | ||||||
886 | $hb2->pack_start($self->{Buttons}{Super}, 0, 0, 0); | ||||||
887 | # $hb2->pack_start($self->{Buttons}{Case}, 0, 0, 0); | ||||||
888 | $hb2->pack_start($self->{Buttons}{Link}, 0, 0, 0); | ||||||
889 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
890 | # $hb2->pack_start($self->{Buttons}{Colour}, 0, 0, 0); | ||||||
891 | # $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
892 | $hb2->pack_start($self->{Buttons}{Pre}, 0, 0, 0); | ||||||
893 | $hb2->pack_start($self->{Buttons}{AsIs}, 0, 0, 0); | ||||||
894 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
895 | $hb2->pack_start($self->{Buttons}{Clear}, 0, 0, 0); | ||||||
896 | |||||||
897 | # PARAGRAPH BLOCK | ||||||
898 | $frame = Gtk2::Frame->new(); | ||||||
899 | $frame->set_shadow_type('etched-in'); | ||||||
900 | $frame->set_border_width(2); | ||||||
901 | $self->{Toolbar}->pack_start($frame, 0, 0, 0); | ||||||
902 | $hb2 = Gtk2::HBox->new(0, 2); | ||||||
903 | $frame->add($hb2); | ||||||
904 | $hb2->pack_start($self->{Buttons}{IndentDown}, 0, 0, 0); | ||||||
905 | $hb2->pack_start($self->{Buttons}{IndentUp}, 0, 0, 0); | ||||||
906 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
907 | $hb2->pack_start($self->{Buttons}{Left}, 0, 0, 0); | ||||||
908 | $hb2->pack_start($self->{Buttons}{Center}, 0, 0, 0); | ||||||
909 | $hb2->pack_start($self->{Buttons}{Right}, 0, 0, 0); | ||||||
910 | $hb2->pack_start($self->{Buttons}{Fill}, 0, 0, 0); | ||||||
911 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
912 | $self->{Buttons}{Heading}->set_width_chars(10); | ||||||
913 | $hb2->pack_start($self->{Buttons}{Heading}, 1, 1, 0); | ||||||
914 | |||||||
915 | # UNDO/REDO GROUP | ||||||
916 | $frame = Gtk2::Frame->new; | ||||||
917 | $frame->set_shadow_type('etched-in'); | ||||||
918 | $frame->set_border_width(2); | ||||||
919 | $self->{Toolbar}->pack_start($frame, 0, 0, 0); | ||||||
920 | $hb2 = Gtk2::HBox->new(0, 2); | ||||||
921 | $frame->add($hb2); | ||||||
922 | $hb2->pack_start($self->{Buttons}{Undo}, 0, 0, 0); | ||||||
923 | $hb2->pack_start($self->{Buttons}{Redo}, 0, 0, 0); | ||||||
924 | |||||||
925 | $self->{Toolbar}->pack_start($self->{Buttons}{DUMP}, 0, 0, 0) | ||||||
926 | if $self->get_property('debug') and defined $self->{Buttons}{DUMP}; | ||||||
927 | $self->{Toolbar}->show_all; | ||||||
928 | $self->attach($self->{Toolbar}, 0, 1, 0, 1, | ||||||
929 | [qw(fill expand)], [qw(fill)], 0, 0); | ||||||
930 | } | ||||||
931 | |||||||
932 | sub _build_fat_toolbar { | ||||||
933 | my $self = shift; | ||||||
934 | # +---------------------------------------------------+ | ||||||
935 | # |+-------------FONT-------------++---PARAGRAPH--++-+| | ||||||
936 | # || FONTV SIZEV | SZ+ SZ- | CLR ||I- I+|L C R F ||U|| | ||||||
937 | # || B I U S sub SUP CASE | BG FG ||HEADING TYPE ||R|| | ||||||
938 | # |+------------------------------++--------------++-+| | ||||||
939 | # +---------------------------------------------------+ | ||||||
940 | $self->{Toolbar} = Gtk2::HBox->new(0, 0); | ||||||
941 | |||||||
942 | # FONT GROUP | ||||||
943 | my $frame = Gtk2::Frame->new('Font'); | ||||||
944 | $frame->set_label_align(0.5, 0.5); | ||||||
945 | $frame->set_shadow_type('etched-in'); | ||||||
946 | my $lab = $frame->get_label_widget; | ||||||
947 | $lab->set_markup('Font'); | ||||||
948 | $self->{Toolbar}->pack_start($frame, 0, 0, 2); | ||||||
949 | my $vbox = Gtk2::VBox->new(0, 0); | ||||||
950 | my $hb2 = Gtk2::HBox->new(0, 0); | ||||||
951 | $frame->add($hb2); | ||||||
952 | $hb2->pack_start($vbox, 0, 0, 2); | ||||||
953 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
954 | $self->{Buttons}{Font}->set_width_chars(0); | ||||||
955 | $hb2->pack_start($self->{Buttons}{Font}, 1, 1, 0); | ||||||
956 | $hb2->pack_start($self->{Buttons}{Size}, 0, 0, 0); | ||||||
957 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
958 | $hb2->pack_start($self->{Buttons}{SizeUp}, 0, 0, 0); | ||||||
959 | $hb2->pack_start($self->{Buttons}{SizeDown}, 0, 0, 0); | ||||||
960 | $vbox->pack_start($hb2, 0, 0, 2); | ||||||
961 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
962 | $hb2->pack_start($self->{Buttons}{Bold}, 0, 0, 0); | ||||||
963 | $hb2->pack_start($self->{Buttons}{Italic}, 0, 0, 0); | ||||||
964 | $hb2->pack_start($self->{Buttons}{Underline}, 0, 0, 0); | ||||||
965 | $hb2->pack_start($self->{Buttons}{Strike}, 0, 0, 0); | ||||||
966 | $hb2->pack_start($self->{Buttons}{Sub}, 0, 0, 0); | ||||||
967 | $hb2->pack_start($self->{Buttons}{Super}, 0, 0, 0); | ||||||
968 | # $hb2->pack_start($self->{Buttons}{Case}, 0, 0, 0); | ||||||
969 | $hb2->pack_start($self->{Buttons}{Link}, 0, 0, 0); | ||||||
970 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
971 | # $hb2->pack_start($self->{Buttons}{Colour}, 0, 0, 0); | ||||||
972 | # $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
973 | $hb2->pack_start($self->{Buttons}{Pre}, 0, 0, 0); | ||||||
974 | $hb2->pack_start($self->{Buttons}{AsIs}, 0, 0, 0); | ||||||
975 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
976 | $hb2->pack_start($self->{Buttons}{Clear}, 0, 0, 0); | ||||||
977 | $vbox->pack_start($hb2, 0, 0, 2); | ||||||
978 | |||||||
979 | # PARA GROUP | ||||||
980 | $frame = Gtk2::Frame->new('Paragraph'); | ||||||
981 | $frame->set_label_align(0.5, 0.5); | ||||||
982 | $frame->set_shadow_type('etched-in'); | ||||||
983 | $lab = $frame->get_label_widget; | ||||||
984 | $lab->set_markup('Paragraph'); | ||||||
985 | $self->{Toolbar}->pack_start($frame, 0, 0, 2); | ||||||
986 | $vbox = Gtk2::VBox->new(0, 0); | ||||||
987 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
988 | $frame->add($hb2); | ||||||
989 | $hb2->pack_start($vbox, 0, 0, 2); | ||||||
990 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
991 | $hb2->pack_start($self->{Buttons}{IndentDown}, 0, 0, 0); | ||||||
992 | $hb2->pack_start($self->{Buttons}{IndentUp}, 0, 0, 0); | ||||||
993 | $hb2->pack_start(Gtk2::VSeparator->new, 0, 0, 2); | ||||||
994 | $hb2->pack_start($self->{Buttons}{Left}, 0, 0, 0); | ||||||
995 | $hb2->pack_start($self->{Buttons}{Center}, 0, 0, 0); | ||||||
996 | $hb2->pack_start($self->{Buttons}{Right}, 0, 0, 0); | ||||||
997 | $hb2->pack_start($self->{Buttons}{Fill}, 0, 0, 0); | ||||||
998 | $vbox->pack_start($hb2, 0, 0, 2); | ||||||
999 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
1000 | $self->{Buttons}{Heading}->set_width_chars(0); | ||||||
1001 | $hb2->pack_start($self->{Buttons}{Heading}, 1, 1, 0); | ||||||
1002 | $vbox->pack_start($hb2, 1, 1, 2); | ||||||
1003 | |||||||
1004 | # UNDO/REDO GROUP | ||||||
1005 | $frame = Gtk2::Frame->new('Undo'); | ||||||
1006 | $frame->set_label_align(0.5, 0.5); | ||||||
1007 | $frame->set_shadow_type('etched-in'); | ||||||
1008 | $lab = $frame->get_label_widget; | ||||||
1009 | $lab->set_markup('Undo'); | ||||||
1010 | $self->{Toolbar}->pack_start($frame, 0, 0, 2); | ||||||
1011 | $vbox = Gtk2::VBox->new(0, 0); | ||||||
1012 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
1013 | $frame->add($hb2); | ||||||
1014 | $hb2->pack_start($vbox, 1, 1, 2); | ||||||
1015 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
1016 | $hb2->pack_start($self->{Buttons}{Undo}, 0, 0, 0); | ||||||
1017 | $vbox->pack_start($hb2, 1, 1, 2); | ||||||
1018 | $hb2 = Gtk2::HBox->new(0, 0); | ||||||
1019 | $hb2->pack_start($self->{Buttons}{Redo}, 0, 0, 0); | ||||||
1020 | $vbox->pack_start($hb2, 1, 1, 2); | ||||||
1021 | |||||||
1022 | $self->{Toolbar}->pack_start($self->{Buttons}{DUMP}, 0, 0, 0) | ||||||
1023 | if $self->get_property('debug') and defined($self->{Buttons}{DUMP}); | ||||||
1024 | $self->{Toolbar}->show_all; | ||||||
1025 | $self->attach($self->{Toolbar}, 0, 1, 0, 1, | ||||||
1026 | [qw(fill expand)], [qw(fill)], 0, 0); | ||||||
1027 | } | ||||||
1028 | |||||||
1029 | ######### | ||||||
1030 | # _build_text - create the text view and initialise it. Also creates cursors | ||||||
1031 | # and connects signals as required | ||||||
1032 | ######### | ||||||
1033 | sub _build_text { | ||||||
1034 | my $self = shift; | ||||||
1035 | my $txt = Gtk2::TextView->new; | ||||||
1036 | my $scr = Gtk2::ScrolledWindow->new; | ||||||
1037 | $scr->set_shadow_type('in'); | ||||||
1038 | $scr->set_policy('automatic', 'automatic'); | ||||||
1039 | $scr->add($txt); | ||||||
1040 | $scr->show_all; | ||||||
1041 | $self->attach($scr, 0, 1, 1, 2, [qw(fill expand)], [qw(fill expand)], 0, 0); | ||||||
1042 | $self->{Text} = $txt; | ||||||
1043 | if ($self->get_property('check-spelling')) { | ||||||
1044 | eval {require Gtk2::Spell}; | ||||||
1045 | if ($@) { | ||||||
1046 | warn("Gtk2::Spell does not appear to be installed!"); | ||||||
1047 | } else { | ||||||
1048 | $self->{GtkSpell} = Gtk2::Spell->new_attach($self->{Text}); | ||||||
1049 | $self->{GtkSpell}->recheck_all; | ||||||
1050 | } | ||||||
1051 | } | ||||||
1052 | my $buf = $txt->get_buffer; | ||||||
1053 | $buf->signal_connect('mark-set' => sub {$self->_on_cursor_move(@_)}); | ||||||
1054 | $buf->signal_connect_after('insert-text' => sub {$self->_on_insert(@_)}); | ||||||
1055 | $buf->signal_connect('delete-range' => sub {$self->_on_delete(@_)}); | ||||||
1056 | $buf->signal_connect_after('delete-range' => sub {$self->_after_delete(@_)}); | ||||||
1057 | $buf->signal_connect('apply-tag' => sub {$self->_on_apply_tag(@_)}); | ||||||
1058 | $buf->signal_connect('remove-tag' => sub {$self->_on_remove_tag(@_)}); | ||||||
1059 | $self->{Cursor}{Current} = 'Text'; | ||||||
1060 | $self->{Cursor}{Text} = Gtk2::Gdk::Cursor->new('xterm'); | ||||||
1061 | $self->{Cursor}{Link} = Gtk2::Gdk::Cursor->new('hand2'); | ||||||
1062 | $self->{Text}->signal_connect(motion_notify_event => | ||||||
1063 | sub {$self->_on_motion_notify(@_)}); | ||||||
1064 | $self->{Text}->signal_connect('focus-out-event' => | ||||||
1065 | sub {$self->_on_unfocus_text}); | ||||||
1066 | $self->{Text}->signal_connect('populate-popup', | ||||||
1067 | sub {$self->_on_popup(@_)}); | ||||||
1068 | } | ||||||
1069 | |||||||
1070 | ########## | ||||||
1071 | # _init_font_list - examines the pango context and sets available fonts, | ||||||
1072 | # the default font and the default size | ||||||
1073 | ########## | ||||||
1074 | sub _init_font_list { | ||||||
1075 | my $self = shift; | ||||||
1076 | my $c = $self->get_pango_context; | ||||||
1077 | $BUTTONS{Font}{Default} = $c->get_font_description->get_family; | ||||||
1078 | $BUTTONS{Font}{Tags} = []; | ||||||
1079 | for my $name (sort {$a cmp $b} map {$_->get_name} $c->list_families) { | ||||||
1080 | push @{$BUTTONS{Font}{Tags}}, $name; | ||||||
1081 | } | ||||||
1082 | $BUTTONS{Size}{Default} = int($c->get_font_description->get_size / 1024); | ||||||
1083 | Gtk2::Ex::WYSIWYG::HTML->set_fonts(@{$BUTTONS{Font}{Tags}}); | ||||||
1084 | Gtk2::Ex::WYSIWYG::HTML->set_default_size($BUTTONS{Size}{Default}); | ||||||
1085 | } | ||||||
1086 | |||||||
1087 | ############################################################################ | ||||||
1088 | # Signal Handlers | ||||||
1089 | ############################################################################ | ||||||
1090 | |||||||
1091 | ########## | ||||||
1092 | # _on_apply_tag - to facilitate undo and redo, record tag applications. | ||||||
1093 | ########## | ||||||
1094 | sub _on_apply_tag { | ||||||
1095 | my $self = shift; | ||||||
1096 | my ($buf, $tag, $s, $e) = @_; | ||||||
1097 | $self->_record_undo(UNDO_APPLY_TAG, $s->get_offset, $e->get_offset, $tag) | ||||||
1098 | if $self->_is_my_tag($tag); | ||||||
1099 | return 0; | ||||||
1100 | } | ||||||
1101 | |||||||
1102 | ########## | ||||||
1103 | # _on_remove_tag - to facilitate undo and redo, record tags removals. | ||||||
1104 | # NOTE: the signal handler recieves a start and end range exactly matching | ||||||
1105 | # what was used in the $buf->remove_tag(...) call, which may be wrong | ||||||
1106 | # if the range includes bits where the tag wasn't applied in the first | ||||||
1107 | # place. All tag removals in code should therefore be done with the | ||||||
1108 | # _remove_tag or _remove_tag_cascade functions within this package | ||||||
1109 | ########## | ||||||
1110 | sub _on_remove_tag { | ||||||
1111 | my $self = shift; | ||||||
1112 | my ($buf, $tag, $s, $e) = @_; | ||||||
1113 | $self->_record_undo(UNDO_REMOVE_TAG, $s->get_offset, $e->get_offset, $tag) | ||||||
1114 | if $self->_is_my_tag($tag); | ||||||
1115 | return 0; | ||||||
1116 | } | ||||||
1117 | |||||||
1118 | ########## | ||||||
1119 | # _on_popup - modify the default popup window to include a Wrap menu | ||||||
1120 | ########## | ||||||
1121 | sub _on_popup { | ||||||
1122 | my $self = shift; | ||||||
1123 | my ($txt, $menu) = @_; | ||||||
1124 | my $currmode = $txt->get_wrap_mode; | ||||||
1125 | my $mt = Gtk2::MenuItem->new('Wrap'); | ||||||
1126 | my $sub = Gtk2::Menu->new; | ||||||
1127 | $mt->set_submenu($sub); | ||||||
1128 | my $grp = undef; | ||||||
1129 | for my $it (['None', 'none'], ['Character', 'char'], | ||||||
1130 | ['Word', 'word'], ['Word, then character', 'word-char']) { | ||||||
1131 | my $mi = Gtk2::RadioMenuItem->new($grp, $it->[0]); | ||||||
1132 | $grp = $mi if not defined $grp; | ||||||
1133 | $mi->set_active($currmode eq $it->[1]); | ||||||
1134 | $mi->signal_connect(activate => sub {$txt->set_wrap_mode($it->[1]) | ||||||
1135 | if $_[0]->get_active; 0}); | ||||||
1136 | $sub->append($mi); | ||||||
1137 | } | ||||||
1138 | $mt->show_all; | ||||||
1139 | $menu->append($mt); | ||||||
1140 | $menu->reorder_child($mt, 7); | ||||||
1141 | return 0; | ||||||
1142 | } | ||||||
1143 | |||||||
1144 | ######### | ||||||
1145 | # _on_cursor_move - if the cursor has moved, update the buttons to reflect the | ||||||
1146 | # new edit mode | ||||||
1147 | ######### | ||||||
1148 | sub _on_cursor_move { | ||||||
1149 | my $self = shift; | ||||||
1150 | my ($buf, $iter, $mark) = @_; | ||||||
1151 | return 0 if $mark->get_name ne 'insert'; | ||||||
1152 | my ($s, $e) = $buf->get_bounds; | ||||||
1153 | return 0 if $s->equal($e); | ||||||
1154 | $self->_set_active_from_text; | ||||||
1155 | $self->_set_buttons_from_active; | ||||||
1156 | return 0; | ||||||
1157 | } | ||||||
1158 | |||||||
1159 | ######### | ||||||
1160 | # _on_insert - make sure that inserted text has the correct tags applied. | ||||||
1161 | # Do nothing if we're in the middle of an undo action | ||||||
1162 | # Remember to record this action if we need to for an undo | ||||||
1163 | ######### | ||||||
1164 | sub _on_insert { | ||||||
1165 | my $self = shift; | ||||||
1166 | my ($buf, $iter, $str) = @_; | ||||||
1167 | return 0 if $self->{Undoing}; # Don't interfere! | ||||||
1168 | my $commit = $self->_start_record_undo; | ||||||
1169 | my $start = $iter->copy; | ||||||
1170 | $start->backward_chars(length $str); | ||||||
1171 | $self->_record_undo(UNDO_INSERT_TEXT, $start->get_offset, $iter->get_offset, | ||||||
1172 | $str); | ||||||
1173 | # Ensure correct tags applied to text inserted | ||||||
1174 | $buf->get_tag_table-> | ||||||
1175 | foreach(sub { | ||||||
1176 | my ($tag) = @_; | ||||||
1177 | return if not $self->_is_my_tag($tag); | ||||||
1178 | if (exists $self->{Active}{$tag->get_property('name')}) { | ||||||
1179 | $self->_apply_tag_cascade($tag, $start, $iter); | ||||||
1180 | } else { | ||||||
1181 | $self->_remove_tag_cascade($tag, $start, $iter); | ||||||
1182 | } | ||||||
1183 | }); | ||||||
1184 | # What if this insert just bridged two paragraphs?! | ||||||
1185 | $self->_normalise_paragraph($start, $iter); | ||||||
1186 | $self->_set_active_from_text; | ||||||
1187 | $self->_commit_record_undo if $commit; | ||||||
1188 | $self->_set_buttons_from_active; | ||||||
1189 | return 0; | ||||||
1190 | } | ||||||
1191 | |||||||
1192 | ########### | ||||||
1193 | # _on_delete - unless we're in the middle of an undo action, record the | ||||||
1194 | # pending change. Don't just record the delete - pre-remove | ||||||
1195 | # any tags applied over the range so an undo doesn't plonk plain | ||||||
1196 | # text back | ||||||
1197 | ########### | ||||||
1198 | sub _on_delete { | ||||||
1199 | my $self = shift; | ||||||
1200 | return 0 if $self->{Undoing}; | ||||||
1201 | my ($buf, $s, $e) = @_; | ||||||
1202 | ++$self->{DeleteCommit} if $self->_start_record_undo; | ||||||
1203 | my $p = $s->copy; | ||||||
1204 | while (1) { | ||||||
1205 | last if $p->compare($e) != -1; | ||||||
1206 | for my $tag ($p->get_tags) { | ||||||
1207 | next if not $self->_is_my_tag($tag); | ||||||
1208 | my $t = $p->copy; | ||||||
1209 | $t = $e->copy if (not $t->forward_to_tag_toggle($tag) or | ||||||
1210 | $t->compare($e) == 1); | ||||||
1211 | $self->_remove_tag($tag, $p, $t); | ||||||
1212 | } | ||||||
1213 | last if not $p->forward_to_tag_toggle(undef); | ||||||
1214 | } | ||||||
1215 | $self->_record_undo(UNDO_DELETE_TEXT, $s->get_offset, $e->get_offset, | ||||||
1216 | $buf->get_text($s, $e, 0)); | ||||||
1217 | 0; | ||||||
1218 | } | ||||||
1219 | |||||||
1220 | ######### | ||||||
1221 | # _after_delete - unless we're in the middle of an undo action, ensure | ||||||
1222 | # paragraph tags are consistent, and make sure the buttons | ||||||
1223 | # reflect the current active state. Also commit the undo | ||||||
1224 | # recording if we have one. | ||||||
1225 | ######### | ||||||
1226 | sub _after_delete { | ||||||
1227 | my $self = shift; | ||||||
1228 | return 0 if $self->{Undoing}; | ||||||
1229 | my ($buf, $s, $e) = @_; | ||||||
1230 | $self->_normalise_paragraph($s, $e); | ||||||
1231 | $self->_set_active_from_text; | ||||||
1232 | $self->_set_buttons_from_active; | ||||||
1233 | if ($self->{DeleteCommit}) { | ||||||
1234 | $self->_commit_record_undo; | ||||||
1235 | --$self->{DeleteCommit}; | ||||||
1236 | } | ||||||
1237 | return 0; | ||||||
1238 | } | ||||||
1239 | |||||||
1240 | sub _on_visibility_notify { | ||||||
1241 | my $self = shift; | ||||||
1242 | $self->_set_cursor; | ||||||
1243 | return 0; | ||||||
1244 | } | ||||||
1245 | |||||||
1246 | sub _on_motion_notify { | ||||||
1247 | my $self = shift; | ||||||
1248 | my ($view, $ev) = @_; | ||||||
1249 | my ($x, $y) = $view->window_to_buffer_coords('widget', $ev->get_coords); | ||||||
1250 | $self->_set_cursor($x, $y); | ||||||
1251 | $view->window->get_pointer; | ||||||
1252 | return 0; | ||||||
1253 | } | ||||||
1254 | |||||||
1255 | sub _on_unfocus_text { | ||||||
1256 | my $self = shift; | ||||||
1257 | $self->{Cursor}{Current} = 'Text'; | ||||||
1258 | $self->{Text}->get_window('text')->set_cursor($self->{Cursor}{Text}); | ||||||
1259 | $self->_tooltip_hide(); | ||||||
1260 | $self->{CurrentLink} = undef; | ||||||
1261 | 0; | ||||||
1262 | } | ||||||
1263 | |||||||
1264 | ########### | ||||||
1265 | # _on_toggle_change - a toggle button has been toggled - reflect the change | ||||||
1266 | ########### | ||||||
1267 | sub _on_toggle_change { | ||||||
1268 | my $self = shift; | ||||||
1269 | return 0 if $self->{Lock}{Buttons}; # Programmatic button change in progress | ||||||
1270 | my ($name) = @_; | ||||||
1271 | my $commit = $self->_start_record_undo; | ||||||
1272 | my $tname = $BUTTONS{$name}{Tag}; | ||||||
1273 | my ($s, $e) = $self->_get_current_bounds_for_tag($tname); | ||||||
1274 | if ($self->{Buttons}{$name}->get_active) { | ||||||
1275 | # Switching on | ||||||
1276 | my $tag = $self->_create_tag($self->_full_tag_name($tname), | ||||||
1277 | %{$TAGS{$tname}{Look}}); | ||||||
1278 | $self->_apply_tag_cascade($tag, $s, $e); | ||||||
1279 | $self->_normalise_paragraph($s, $e) | ||||||
1280 | if ($tname eq 'asis' or $tname eq 'pre') and not $s->equal($e); | ||||||
1281 | $self->_set_active_from_text if not $s->equal($e); | ||||||
1282 | $self->{Active}{$tag->get_property('name')} = undef; | ||||||
1283 | $self->_set_buttons_from_active; | ||||||
1284 | } else { | ||||||
1285 | # Switching off | ||||||
1286 | my $tag = $self->_full_tag_name($tname); | ||||||
1287 | $self->_remove_tag_cascade($tag, $s, $e); | ||||||
1288 | $self->_set_active_from_text if not $s->equal($e); | ||||||
1289 | delete($self->{Active}{$tag}); | ||||||
1290 | $self->_set_buttons_from_active; | ||||||
1291 | } | ||||||
1292 | $self->_commit_record_undo if $commit; | ||||||
1293 | return 0; | ||||||
1294 | } | ||||||
1295 | |||||||
1296 | ########### | ||||||
1297 | # _on_multi_toggle_change - a toggle button has been toggled, and it is a | ||||||
1298 | # 'multi' tag (ie, makes tagname_X tags rather than | ||||||
1299 | # just one tagname tag). Uses the ToggleOn and | ||||||
1300 | # ToggleOff tag definitions. | ||||||
1301 | ########### | ||||||
1302 | sub _on_multi_toggle_change { | ||||||
1303 | my $self = shift; | ||||||
1304 | return 0 if $self->{Lock}{Buttons}; | ||||||
1305 | my ($bname) = @_; | ||||||
1306 | my $commit = $self->_start_record_undo; | ||||||
1307 | my $tname = $BUTTONS{$bname}{Tag}; | ||||||
1308 | my ($s, $e) = $self->_get_current_bounds_for_tag($tname); | ||||||
1309 | if ($self->{Buttons}{$bname}->get_active) { | ||||||
1310 | die "Multi tag without toggle on code '$tname'!" | ||||||
1311 | if not exists $TAGS{$tname}{ToggleOn}; | ||||||
1312 | $TAGS{$tname}{ToggleOn}->($self, $bname, $s, $e); | ||||||
1313 | } else { | ||||||
1314 | die "Multi tag without toggle off code '$tname'!" | ||||||
1315 | if not exists $TAGS{$tname}{ToggleOff}; | ||||||
1316 | $TAGS{$tname}{ToggleOff}->($self, $bname, $s, $e); | ||||||
1317 | } | ||||||
1318 | $self->_commit_record_undo if $commit; | ||||||
1319 | return 0; | ||||||
1320 | } | ||||||
1321 | |||||||
1322 | sub _on_button_click { | ||||||
1323 | my $self = shift; | ||||||
1324 | return 0 if $self->{Lock}{Buttons}; | ||||||
1325 | my ($bname) = @_; | ||||||
1326 | my $tname = $BUTTONS{$bname}{Tag}; | ||||||
1327 | die "No code for tag '$tname'!" if not exists $TAGS{$tname}{Activate}; | ||||||
1328 | my $commit = $self->_start_record_undo; | ||||||
1329 | $TAGS{$tname}{Activate}->($self, $bname, | ||||||
1330 | $self->_get_current_bounds_for_tag($tname)); | ||||||
1331 | $self->_commit_record_undo if $commit; | ||||||
1332 | return 0; | ||||||
1333 | } | ||||||
1334 | |||||||
1335 | sub _on_menu_change { | ||||||
1336 | my $self = shift; | ||||||
1337 | my ($bname, $wid, $display, $tname) = @_; | ||||||
1338 | return 0 if $self->{Lock}{Buttons}; | ||||||
1339 | return 0 if $self->{Buttons}{$bname}->get_inconsistant; # make no changes! | ||||||
1340 | my $commit = $self->_start_record_undo; | ||||||
1341 | my ($s, $e); | ||||||
1342 | my $buf = $self->{Text}->get_buffer; | ||||||
1343 | for my $tag (@{$BUTTONS{$bname}{Tags}}) { | ||||||
1344 | next if not exists $TAGS{$tag->[0]}; | ||||||
1345 | ($s, $e) = $self->_get_current_bounds_for_tag($tag->[0]) | ||||||
1346 | if not defined $s; | ||||||
1347 | last if $s->equal($e); | ||||||
1348 | $self->_remove_tag_cascade($self->_full_tag_name($tag->[0]), $s, $e); | ||||||
1349 | } | ||||||
1350 | my $ftname = $self->_full_tag_name($tname); | ||||||
1351 | my $tag = $self->_create_tag($ftname, %{$TAGS{$tname}{Look}}) | ||||||
1352 | if $display ne $BUTTONS{$bname}{Default}; | ||||||
1353 | if ($s->equal($e)) { | ||||||
1354 | for my $tag (@{$BUTTONS{$bname}{Tags}}) { | ||||||
1355 | delete($self->{Active}{$self->_full_tag_name($tag->[0])}); | ||||||
1356 | } | ||||||
1357 | $self->{Active}{$ftname} = undef; | ||||||
1358 | $self->_set_buttons_from_active; | ||||||
1359 | } else { | ||||||
1360 | $self->_apply_tag_cascade($tag, $s, $e) | ||||||
1361 | if $display ne $BUTTONS{$bname}{Default}; | ||||||
1362 | # Update subscript and superscript over this range! | ||||||
1363 | # Maybe meld this into apply_tag_cascade? | ||||||
1364 | if ($tname =~ /^h[1-5]\z/) { | ||||||
1365 | $self->_update_superscript($s, $e, undef, $TAGS{$tname}{Look}{scale}); | ||||||
1366 | $self->_update_subscript($s, $e, undef, $TAGS{$tname}{Look}{scale}); | ||||||
1367 | } elsif ($tname eq 'h0') { | ||||||
1368 | $self->_update_superscript($s, $e, undef, 1); | ||||||
1369 | $self->_update_subscript($s, $e, undef, 1); | ||||||
1370 | } | ||||||
1371 | $self->{Active}{$ftname} = undef; | ||||||
1372 | $self->_set_buttons_from_active; | ||||||
1373 | } | ||||||
1374 | $self->_commit_record_undo if $commit; | ||||||
1375 | return 0; | ||||||
1376 | } | ||||||
1377 | |||||||
1378 | sub _on_font_change { | ||||||
1379 | my $self = shift; | ||||||
1380 | my ($bname, $wid, $display, $tname) = @_; | ||||||
1381 | return 0 if $self->{Lock}{Buttons}; | ||||||
1382 | return 0 if $self->{Buttons}{$bname}->get_inconsistant; # make no changes! | ||||||
1383 | my $commit = $self->_start_record_undo; | ||||||
1384 | my $buf = $self->{Text}->get_buffer; | ||||||
1385 | my ($s, $e) = $self->_get_current_bounds_for_tag('font'); | ||||||
1386 | # Remove any current font from that range | ||||||
1387 | { | ||||||
1388 | my @rem; | ||||||
1389 | my $tt = $buf->get_tag_table; | ||||||
1390 | $tt->foreach(sub { | ||||||
1391 | push @rem, $_[0] if | ||||||
1392 | $self->_short_tag_name($_[0]) eq 'font'; | ||||||
1393 | }); | ||||||
1394 | for my $rem (@rem) { | ||||||
1395 | $self->_remove_tag($rem, $s, $e); | ||||||
1396 | } | ||||||
1397 | } | ||||||
1398 | my $ftname = $self->_full_tag_name('font', $tname); | ||||||
1399 | my $tag = $self->_create_tag($ftname, family => $tname) | ||||||
1400 | if $display ne $BUTTONS{$bname}{Default}; | ||||||
1401 | if ($s->equal($e)) { | ||||||
1402 | for my $tag (@{$BUTTONS{$bname}{Tags}}) { | ||||||
1403 | delete($self->{Active}{$self->_full_tag_name('font', $tag)}); | ||||||
1404 | } | ||||||
1405 | } elsif ($display ne $BUTTONS{$bname}{Default}) { | ||||||
1406 | $self->_apply_tag_cascade($tag, $s, $e); | ||||||
1407 | } | ||||||
1408 | $self->{Active}{$ftname} = undef; | ||||||
1409 | $self->_set_buttons_from_active; | ||||||
1410 | $self->_commit_record_undo if $commit; | ||||||
1411 | return 0; | ||||||
1412 | } | ||||||
1413 | |||||||
1414 | sub _on_size_change { | ||||||
1415 | my $self = shift; | ||||||
1416 | return 0 if $self->{Lock}{Buttons}; | ||||||
1417 | my ($name, $wid, $size) = @_; | ||||||
1418 | return 0 if $size !~ /\d/ or not $size; | ||||||
1419 | my $commit = $self->_start_record_undo; | ||||||
1420 | my $buf = $self->{Text}->get_buffer; | ||||||
1421 | my $tname = $BUTTONS{$name}{Tag}; | ||||||
1422 | my ($s, $e) = $self->_get_current_bounds_for_tag($tname); | ||||||
1423 | my $nosel = $s->equal($e); | ||||||
1424 | if (not $nosel) { | ||||||
1425 | $buf->get_tag_table-> | ||||||
1426 | foreach(sub { | ||||||
1427 | my ($tag) = @_; | ||||||
1428 | return if not $self->_is_my_tag($tag); | ||||||
1429 | $self->_remove_tag_cascade($tag, $s, $e) | ||||||
1430 | if $self->_short_tag_name($tag) eq $tname; | ||||||
1431 | }); | ||||||
1432 | # Update super/subscript tags for this range! | ||||||
1433 | $self->_update_subscript($s, $e, $size); | ||||||
1434 | $self->_update_superscript($s, $e, $size); | ||||||
1435 | } | ||||||
1436 | my $tag = $self->_create_tag($self->_full_tag_name($tname, $size), | ||||||
1437 | size => $size * 1024); | ||||||
1438 | if ($nosel) { | ||||||
1439 | for my $k (keys %{$self->{Active}}) { | ||||||
1440 | delete($self->{Active}{$k}) | ||||||
1441 | if $self->_short_tag_name($k) eq $BUTTONS{$name}{Tag}; | ||||||
1442 | } | ||||||
1443 | $self->{Active}{$tag->get_property('name')} = undef; | ||||||
1444 | } else { | ||||||
1445 | $self->_apply_tag_cascade($tag, $s, $e); | ||||||
1446 | $self->_set_active_from_text; | ||||||
1447 | } | ||||||
1448 | $self->_set_buttons_from_active; | ||||||
1449 | $self->_commit_record_undo if $commit; | ||||||
1450 | return 0; | ||||||
1451 | } | ||||||
1452 | |||||||
1453 | # Callbacks for specific buttons | ||||||
1454 | |||||||
1455 | sub _sup_sub_scan { | ||||||
1456 | my $self = shift; | ||||||
1457 | my ($s, $e, $type, $force) = @_; | ||||||
1458 | my ($sz, $sc); | ||||||
1459 | for my $tag ($s->get_tags) { | ||||||
1460 | next if not $self->_is_my_tag($tag); | ||||||
1461 | my $name = $self->_short_tag_name($tag); | ||||||
1462 | if ($name eq 'superscript' or $name eq 'subscript') { | ||||||
1463 | $self->_remove_tag_cascade($tag, $s, $e); | ||||||
1464 | next; | ||||||
1465 | } | ||||||
1466 | if (not defined $sz and $name eq 'size') { | ||||||
1467 | ($sz) = $self->_tag_args($tag, 1); | ||||||
1468 | } elsif (not defined $sc and $name =~ /^h[1-5]\z/) { | ||||||
1469 | $sc = $TAGS{$name}{Look}{scale}; | ||||||
1470 | } | ||||||
1471 | } | ||||||
1472 | $sz = $BUTTONS{Size}{Default} if not defined $sz; | ||||||
1473 | $sc = 1 if not $sc; | ||||||
1474 | my $n = $s->copy; | ||||||
1475 | $n->forward_to_tag_toggle(undef); | ||||||
1476 | $n = $e->copy if $n->compare($e) == 1; | ||||||
1477 | $self->_apply_tag_cascade($self->_create_sub_super_tag($type, $sz, $sc), | ||||||
1478 | $s, $n); | ||||||
1479 | return $n; | ||||||
1480 | } | ||||||
1481 | |||||||
1482 | sub _create_sub_super_tag { | ||||||
1483 | my $self = shift; | ||||||
1484 | my ($type, $size, $scale) = @_; | ||||||
1485 | my $rise = ($type eq 'superscript' ? 0.75 : -0.25); | ||||||
1486 | $rise = int($size * $scale * $rise * 1024); | ||||||
1487 | $self->_create_tag($self->_full_tag_name($type, $size, $scale), | ||||||
1488 | scale => 0.5, rise => $rise); | ||||||
1489 | } | ||||||
1490 | |||||||
1491 | sub _superscript_on { | ||||||
1492 | my $self = shift; | ||||||
1493 | my ($s, $e) = @_; | ||||||
1494 | my $buf = $self->{Text}->get_buffer; | ||||||
1495 | my $p = $s->copy; | ||||||
1496 | while (1) { | ||||||
1497 | $p = $self->_sup_sub_scan($p, $e, 'superscript'); | ||||||
1498 | last if $p->compare($e) != -1; | ||||||
1499 | } | ||||||
1500 | $self->_set_active_from_text; | ||||||
1501 | $self->_set_buttons_from_active; | ||||||
1502 | } | ||||||
1503 | |||||||
1504 | sub _superscript_off { | ||||||
1505 | my $self = shift; | ||||||
1506 | my ($s, $e) = @_; | ||||||
1507 | my $buf = $self->{Text}->get_buffer; | ||||||
1508 | $buf->get_tag_table-> | ||||||
1509 | foreach(sub { | ||||||
1510 | my ($tag) = @_; | ||||||
1511 | return if (not $self->_is_my_tag($tag) or | ||||||
1512 | $self->_short_tag_name($tag) ne 'superscript'); | ||||||
1513 | $self->_remove_tag_cascade($tag, $s, $e); | ||||||
1514 | }); | ||||||
1515 | $self->_set_active_from_text; | ||||||
1516 | $self->_set_buttons_from_active; | ||||||
1517 | } | ||||||
1518 | |||||||
1519 | sub _update_superscript { | ||||||
1520 | my $self = shift; | ||||||
1521 | my ($s, $e, $force_size, $force_scale) = @_; | ||||||
1522 | $s = $s->copy; | ||||||
1523 | my $buf = $self->{Text}->get_buffer; | ||||||
1524 | while (1) { | ||||||
1525 | last if $s->compare($e) != -1; | ||||||
1526 | my ($size, $scale, $curr, $csize, $cscale) = | ||||||
1527 | ($BUTTONS{Size}{Default}, 1, undef, undef, undef); | ||||||
1528 | for my $tag ($s->get_tags) { | ||||||
1529 | next if not $self->_is_my_tag($tag); | ||||||
1530 | my $name = $self->_short_tag_name($tag); | ||||||
1531 | if ($name eq 'size') { | ||||||
1532 | ($size) = $self->_tag_args($tag, 1); | ||||||
1533 | } elsif ($name =~ /^h[1-5]\z/) { | ||||||
1534 | $scale = $TAGS{$name}{Look}{scale}; | ||||||
1535 | } elsif ($name eq 'superscript') { | ||||||
1536 | $curr = $tag; | ||||||
1537 | ($csize, $cscale) = $self->_tag_args($tag, 2); | ||||||
1538 | } | ||||||
1539 | } | ||||||
1540 | $scale = 1 if not $scale; | ||||||
1541 | $size = $force_size if defined $force_size; | ||||||
1542 | $scale = $force_scale if defined $force_scale; | ||||||
1543 | my $t = $s->copy; | ||||||
1544 | $t = $e->copy if not $t->forward_to_tag_toggle(undef); | ||||||
1545 | if (defined($curr) and ($csize != $size or $cscale != $scale)) { | ||||||
1546 | $self->_remove_tag($curr, $s, $t); | ||||||
1547 | $self->_apply_tag($self->_create_sub_super_tag('superscript', | ||||||
1548 | $size, $scale), $s, $t); | ||||||
1549 | } | ||||||
1550 | $s = $t; | ||||||
1551 | } | ||||||
1552 | } | ||||||
1553 | |||||||
1554 | sub _subscript_on { | ||||||
1555 | my $self = shift; | ||||||
1556 | my ($s, $e) = @_; | ||||||
1557 | my $buf = $self->{Text}->get_buffer; | ||||||
1558 | my $p = $s->copy; | ||||||
1559 | while (1) { | ||||||
1560 | $p = $self->_sup_sub_scan($p, $e, 'subscript'); | ||||||
1561 | last if $p->compare($e) != -1; | ||||||
1562 | } | ||||||
1563 | $self->_set_active_from_text; | ||||||
1564 | $self->_set_buttons_from_active; | ||||||
1565 | } | ||||||
1566 | |||||||
1567 | sub _subscript_off { | ||||||
1568 | my $self = shift; | ||||||
1569 | my ($s, $e) = @_; | ||||||
1570 | my $buf = $self->{Text}->get_buffer; | ||||||
1571 | $buf->get_tag_table-> | ||||||
1572 | foreach(sub { | ||||||
1573 | my ($tag) = @_; | ||||||
1574 | return if (not $self->_is_my_tag($tag) or | ||||||
1575 | $self->_short_tag_name($tag) ne 'subscript'); | ||||||
1576 | $self->_remove_tag_cascade($tag, $s, $e); | ||||||
1577 | }); | ||||||
1578 | $self->_set_active_from_text; | ||||||
1579 | $self->_set_buttons_from_active; | ||||||
1580 | } | ||||||
1581 | |||||||
1582 | sub _update_subscript { | ||||||
1583 | my $self = shift; | ||||||
1584 | my ($s, $e, $force_size, $force_scale) = @_; | ||||||
1585 | $s = $s->copy; | ||||||
1586 | my $buf = $self->{Text}->get_buffer; | ||||||
1587 | while (1) { | ||||||
1588 | last if $s->compare($e) != -1; | ||||||
1589 | my ($size, $scale, $curr, $csize, $cscale) = | ||||||
1590 | ($BUTTONS{Size}{Default}, 1, undef, undef, undef); | ||||||
1591 | for my $tag ($s->get_tags) { | ||||||
1592 | next if not $self->_is_my_tag($tag); | ||||||
1593 | my $name = $self->_short_tag_name($tag); | ||||||
1594 | if ($name eq 'size') { | ||||||
1595 | ($size) = $self->_tag_args($tag, 1); | ||||||
1596 | } elsif ($name =~ /^h[1-5]\z/) { | ||||||
1597 | $scale = $TAGS{$name}{Look}{scale}; | ||||||
1598 | } elsif ($name eq 'subscript') { | ||||||
1599 | $curr = $tag; | ||||||
1600 | ($csize, $cscale) = $self->_tag_args($tag, 2); | ||||||
1601 | } | ||||||
1602 | } | ||||||
1603 | $scale = 1 if not $scale; | ||||||
1604 | $size = $force_size if defined $force_size; | ||||||
1605 | $scale = $force_scale if defined $force_scale; | ||||||
1606 | my $t = $s->copy; | ||||||
1607 | $t = $e->copy if not $t->forward_to_tag_toggle(undef); | ||||||
1608 | if (defined($curr) and ($csize != $size or $cscale != $scale)) { | ||||||
1609 | $self->_remove_tag($curr, $s, $t); | ||||||
1610 | $self->_apply_tag($self->_create_sub_super_tag('subscript', | ||||||
1611 | $size, $scale), $s, $t); | ||||||
1612 | } | ||||||
1613 | $s = $t; | ||||||
1614 | } | ||||||
1615 | } | ||||||
1616 | |||||||
1617 | sub _indent_up { | ||||||
1618 | my $self = shift; | ||||||
1619 | my ($s, $e) = @_; | ||||||
1620 | my ($ps, $pe) = $self->_get_current_bounds_for_tag('indent'); | ||||||
1621 | my $buf = $self->{Text}->get_buffer; | ||||||
1622 | while (1) { | ||||||
1623 | last if $ps->compare($pe) != -1; | ||||||
1624 | my $curr; | ||||||
1625 | for my $tag ($ps->get_tags) { | ||||||
1626 | next if not $self->_is_my_tag($tag); | ||||||
1627 | my ($name, $val) = $self->_tag_name_args($tag, 1); | ||||||
1628 | next if $name ne 'indent'; | ||||||
1629 | $curr = $val; | ||||||
1630 | last; | ||||||
1631 | } | ||||||
1632 | my $t = $ps->copy; | ||||||
1633 | $ps = $pe if not $ps->forward_to_tag_toggle(undef); | ||||||
1634 | if (defined($curr)) { | ||||||
1635 | $self->_remove_tag($self->_full_tag_name('indent', $curr), $t, $ps); | ||||||
1636 | ++$curr; | ||||||
1637 | } else { | ||||||
1638 | $curr = 0; | ||||||
1639 | } | ||||||
1640 | $self->_apply_tag($self->_create_tag($self->_full_tag_name('indent', | ||||||
1641 | $curr), | ||||||
1642 | 'left-margin' => 32 * ($curr + 1)), | ||||||
1643 | $t, $ps); | ||||||
1644 | } | ||||||
1645 | return 0; | ||||||
1646 | } | ||||||
1647 | |||||||
1648 | sub _indent_down { | ||||||
1649 | my $self = shift; | ||||||
1650 | my ($s, $e) = @_; | ||||||
1651 | my ($ps, $pe) = $self->_get_current_bounds_for_tag('indent'); | ||||||
1652 | my $buf = $self->{Text}->get_buffer; | ||||||
1653 | while (1) { | ||||||
1654 | last if $ps->compare($pe) != -1; | ||||||
1655 | my $curr; | ||||||
1656 | for my $tag ($ps->get_tags) { | ||||||
1657 | next if not $self->_is_my_tag($tag); | ||||||
1658 | my ($name, $val) = $self->_tag_name_args($tag, 1); | ||||||
1659 | next if $name ne 'indent'; | ||||||
1660 | $curr = $val; | ||||||
1661 | last; | ||||||
1662 | } | ||||||
1663 | my $t = $ps->copy; | ||||||
1664 | $ps = $pe if not $ps->forward_to_tag_toggle(undef); | ||||||
1665 | next if not defined $curr; | ||||||
1666 | $self->_remove_tag($self->_full_tag_name('indent', $curr), $t, $ps); | ||||||
1667 | next if not $curr; | ||||||
1668 | --$curr; | ||||||
1669 | $self->_apply_tag($self->_create_tag($self->_full_tag_name('indent', | ||||||
1670 | $curr), | ||||||
1671 | 'left-margin' => 32 * ($curr + 1)), | ||||||
1672 | $t, $ps); | ||||||
1673 | } | ||||||
1674 | return 0; | ||||||
1675 | } | ||||||
1676 | |||||||
1677 | sub _link_on { | ||||||
1678 | my $self = shift; | ||||||
1679 | my ($s, $e) = @_; | ||||||
1680 | my $buf = $self->{Text}->get_buffer; | ||||||
1681 | my $txt = $buf->get_text($s, $e, 0); | ||||||
1682 | my $target = $txt; | ||||||
1683 | ($txt, $target) = $self->_get_link_target($txt, $target); | ||||||
1684 | return 0 if not defined $txt; # What about length?! | ||||||
1685 | my $tag = $self->_create_link($target); | ||||||
1686 | if ($s->equal($e)) { # No selection | ||||||
1687 | my $here = $buf->get_iter_at_mark($buf->get_insert); | ||||||
1688 | my $s = $here->get_offset; | ||||||
1689 | $buf->insert($here, $txt); | ||||||
1690 | $s = $buf->get_iter_at_offset($s); | ||||||
1691 | $e = $s->copy; | ||||||
1692 | $e->forward_chars(length($txt)); | ||||||
1693 | $self->_apply_tag_cascade($tag, $s, $e); | ||||||
1694 | } else { | ||||||
1695 | my $off = $s->get_offset; | ||||||
1696 | $buf->delete($s, $e); ## GET TAGS OVER THIS RANGE | ||||||
1697 | $s = $buf->get_iter_at_offset($off); | ||||||
1698 | $buf->insert($s, $txt); ## APPLY TAGS OVER THIS RANGE | ||||||
1699 | $s = $buf->get_iter_at_offset($off); | ||||||
1700 | $e = $s->copy; | ||||||
1701 | $e->forward_chars(length($txt)); | ||||||
1702 | $self->_apply_tag_cascade($tag, $s, $e); | ||||||
1703 | $buf->select_range($s, $e); | ||||||
1704 | } | ||||||
1705 | } | ||||||
1706 | |||||||
1707 | sub _create_link { | ||||||
1708 | my $self = shift; | ||||||
1709 | my ($target) = @_; | ||||||
1710 | $self->{LinkID} = 0 if not exists $self->{LinkID}; | ||||||
1711 | my $tag = $self->_create_tag($self->_full_tag_name('link', | ||||||
1712 | $self->{LinkID}++), | ||||||
1713 | %{$TAGS{link}{Look}}); | ||||||
1714 | $tag->{Target} = $target; | ||||||
1715 | return $tag; | ||||||
1716 | } | ||||||
1717 | |||||||
1718 | sub _link_off { | ||||||
1719 | my $self = shift; | ||||||
1720 | my ($s, $e) = @_; | ||||||
1721 | my $buf = $self->{Text}->get_buffer; | ||||||
1722 | $buf->get_tag_table->foreach(sub { | ||||||
1723 | my ($tag) = @_; | ||||||
1724 | $self->_remove_tag_cascade($tag, $s, $e) | ||||||
1725 | if ($self->_is_my_tag($tag) and | ||||||
1726 | $self->_short_tag_name($tag) eq 'link'); | ||||||
1727 | }) if not $s->equal($e); | ||||||
1728 | } | ||||||
1729 | |||||||
1730 | sub _get_link_target { | ||||||
1731 | my $self = shift; | ||||||
1732 | my ($txt, $target) = @_; | ||||||
1733 | my $win = $self; | ||||||
1734 | while (1) { | ||||||
1735 | last if $win->isa('Gtk2::Window'); | ||||||
1736 | $win = $win->get_parent; | ||||||
1737 | last if not defined $win; | ||||||
1738 | } | ||||||
1739 | my $dlg = Gtk2::Dialog->new("Insert link...", $win, | ||||||
1740 | [qw(modal destroy-with-parent)]); | ||||||
1741 | my $cancel = $dlg->add_button('gtk-cancel' => 'cancel'); | ||||||
1742 | my $ok = $dlg->add_button('gtk-ok' => 'ok'); | ||||||
1743 | my $tbl = Gtk2::Table->new(3, 2, 0); | ||||||
1744 | my $label = Gtk2::Label->new("Define your link text and destination"); | ||||||
1745 | $tbl->attach($label, 0, 2, 0, 1, [qw(fill expand)], [], 4, 4); | ||||||
1746 | my ($etxt, $elnk); | ||||||
1747 | for my $dat ([\$etxt, 'Text:', $txt, 1], | ||||||
1748 | [\$elnk, 'Link:', $target, 2]) { | ||||||
1749 | my ($er, $lb, $tx, $i) = @$dat; | ||||||
1750 | my $lab = Gtk2::Label->new($lb); | ||||||
1751 | $tbl->attach($lab, 0, 1, $i, $i + 1, [], [qw(fill)], 4, 4); | ||||||
1752 | $$er = Gtk2::Entry->new; | ||||||
1753 | $$er->set_text($tx); | ||||||
1754 | $$er->signal_connect(activate => | ||||||
1755 | sub {$ok->clicked if $ok->sensitive; 0}); | ||||||
1756 | $$er->signal_connect(changed => | ||||||
1757 | sub { | ||||||
1758 | $ok->set_sensitive(length($etxt->get_text) and | ||||||
1759 | length($elnk->get_text)); | ||||||
1760 | 0; | ||||||
1761 | }); | ||||||
1762 | $tbl->attach($$er, 1, 2, $i, $i + 1, [], [qw(fill expand)], 4, 4); | ||||||
1763 | } | ||||||
1764 | $ok->set_sensitive(0) if not length($txt) or not length($target); | ||||||
1765 | (length($txt) ? $elnk : $etxt)->grab_focus; | ||||||
1766 | $tbl->show_all; | ||||||
1767 | eval {$dlg->get_content_area->add($tbl)}; | ||||||
1768 | $dlg->vbox->add($tbl) if $@; | ||||||
1769 | $dlg->set_default_response('ok'); | ||||||
1770 | my $res = $dlg->run; | ||||||
1771 | if ($res ne 'ok') { | ||||||
1772 | $dlg->destroy; | ||||||
1773 | return; | ||||||
1774 | } | ||||||
1775 | $txt = $etxt->get_text; | ||||||
1776 | $target = $elnk->get_text; | ||||||
1777 | $dlg->destroy; | ||||||
1778 | return ($txt, $target); | ||||||
1779 | } | ||||||
1780 | |||||||
1781 | sub _increase_size { | ||||||
1782 | my $self = shift; | ||||||
1783 | if (not $self->{Buttons}{Size}->get_inconsistant) { | ||||||
1784 | $self->{Buttons}{Size}->up_value; | ||||||
1785 | return 0; | ||||||
1786 | } | ||||||
1787 | my ($s, $e) = $self->_get_current_bounds_for_tag('size'); | ||||||
1788 | my $buf = $self->{Text}->get_buffer; | ||||||
1789 | while (1) { | ||||||
1790 | last if $s->compare($e) != -1; | ||||||
1791 | my $size = $BUTTONS{Size}{Default}; | ||||||
1792 | for my $tag ($s->get_tags) { | ||||||
1793 | next if not $self->_is_my_tag($tag); | ||||||
1794 | my ($name, $val) = $self->_tag_name_args($tag, 1); | ||||||
1795 | next if $name ne 'size'; | ||||||
1796 | $size = $val; | ||||||
1797 | last; | ||||||
1798 | } | ||||||
1799 | my $t = $s->copy; | ||||||
1800 | $s = $e if not $s->forward_to_tag_toggle(undef); | ||||||
1801 | $self->_remove_tag($self->_full_tag_name('size', $size), $t, $s); | ||||||
1802 | $size = $self->{Buttons}{Size}->next_value_up($size); | ||||||
1803 | $self->_apply_tag($self->_create_tag($self->_full_tag_name('size', $size), | ||||||
1804 | size => $size * 1024), $t, $s); | ||||||
1805 | } | ||||||
1806 | $self->_set_active_from_text; | ||||||
1807 | $self->_set_buttons_from_active; | ||||||
1808 | return 0; | ||||||
1809 | } | ||||||
1810 | |||||||
1811 | sub _decrease_size { | ||||||
1812 | my $self = shift; | ||||||
1813 | if (not $self->{Buttons}{Size}->get_inconsistant) { | ||||||
1814 | $self->{Buttons}{Size}->down_value; | ||||||
1815 | return 0; | ||||||
1816 | } | ||||||
1817 | # Selection, and with differing sizes | ||||||
1818 | my ($s, $e) = $self->_get_current_bounds_for_tag('size'); | ||||||
1819 | my $buf = $self->{Text}->get_buffer; | ||||||
1820 | while (1) { | ||||||
1821 | last if $s->compare($e) != -1; | ||||||
1822 | my $size = $BUTTONS{Size}{Default}; | ||||||
1823 | for my $tag ($s->get_tags) { | ||||||
1824 | next if not $self->_is_my_tag($tag); | ||||||
1825 | my ($name, $val) = $self->_tag_name_args($tag, 1); | ||||||
1826 | next if $name ne 'size'; | ||||||
1827 | $size = $val; | ||||||
1828 | last; | ||||||
1829 | } | ||||||
1830 | my $t = $s->copy; | ||||||
1831 | $s = $e if not $s->forward_to_tag_toggle(undef); | ||||||
1832 | $self->_remove_tag($self->_full_tag_name('size', $size), $t, $s); | ||||||
1833 | $size = $self->{Buttons}{Size}->next_value_down($size); | ||||||
1834 | $self->_apply_tag($self->_create_tag($self->_full_tag_name('size', $size), | ||||||
1835 | size => $size * 1024), $t, $s); | ||||||
1836 | } | ||||||
1837 | $self->_set_active_from_text; | ||||||
1838 | $self->_set_buttons_from_active; | ||||||
1839 | return 0; | ||||||
1840 | } | ||||||
1841 | |||||||
1842 | sub _clear_font_formatting { | ||||||
1843 | my $self = shift; | ||||||
1844 | my ($s, $e) = @_; | ||||||
1845 | my $buf = $self->{Text}->get_buffer; | ||||||
1846 | if ($s->equal($e)) { | ||||||
1847 | # remove all non-paragraph tags | ||||||
1848 | for my $tname (keys %{$self->{Active}}) { | ||||||
1849 | my $rname = $self->_short_tag_name($tname); | ||||||
1850 | next if not exists $TAGS{$rname} or $TAGS{$rname}{Class} eq 'paragraph'; | ||||||
1851 | delete($self->{Active}{$tname}); | ||||||
1852 | } | ||||||
1853 | $self->_set_active_from_text if not $s->equal($buf->get_end_iter); | ||||||
1854 | } else { | ||||||
1855 | $buf->get_tag_table->foreach(sub { | ||||||
1856 | my ($tag) = @_; | ||||||
1857 | return if not $self->_is_my_tag($tag); | ||||||
1858 | my $name = $self->_short_tag_name($tag); | ||||||
1859 | return | ||||||
1860 | if (not exists $TAGS{$name} or | ||||||
1861 | $TAGS{$name}{Class} eq 'paragraph'); | ||||||
1862 | $self->_remove_tag_cascade($tag, $s, $e); | ||||||
1863 | }); | ||||||
1864 | $self->_set_active_from_text; | ||||||
1865 | } | ||||||
1866 | $self->_set_buttons_from_active; | ||||||
1867 | } | ||||||
1868 | |||||||
1869 | # Undo and Redo | ||||||
1870 | |||||||
1871 | sub _start_record_undo { | ||||||
1872 | my $self = shift; | ||||||
1873 | return 0 if $self->{Undoing} or defined $self->{Record}; | ||||||
1874 | $self->{Record} = []; | ||||||
1875 | return 1; | ||||||
1876 | } | ||||||
1877 | |||||||
1878 | sub _record_undo { | ||||||
1879 | my $self = shift; | ||||||
1880 | return if $self->{Undoing} or not defined $self->{Record}; | ||||||
1881 | my ($act, $start, $end, @dat) = @_; | ||||||
1882 | push @{$self->{Record}}, [$act, $start, $end, @dat]; | ||||||
1883 | } | ||||||
1884 | |||||||
1885 | sub _commit_record_undo { | ||||||
1886 | my $self = shift; | ||||||
1887 | return 0 if $self->{Undoing}; | ||||||
1888 | if (defined($self->{Record}) and scalar(@{$self->{Record}})) { | ||||||
1889 | push @{$self->{UndoStack}}, $self->{Record}; | ||||||
1890 | my $max = $self->{Properties}{undo_stack}; | ||||||
1891 | shift @{$self->{UndoStack}} if ($max and | ||||||
1892 | scalar(@{$self->{UndoStack}}) > $max); | ||||||
1893 | $self->{RedoStack} = []; ### | ||||||
1894 | } | ||||||
1895 | $self->{Record} = undef; | ||||||
1896 | } | ||||||
1897 | |||||||
1898 | sub _rollback_record_undo { | ||||||
1899 | my $self = shift; | ||||||
1900 | $self->{Record} = undef; | ||||||
1901 | } | ||||||
1902 | |||||||
1903 | # Tag handling | ||||||
1904 | |||||||
1905 | sub _create_tag { | ||||||
1906 | my $self = shift; | ||||||
1907 | my ($name, %opts) = @_; | ||||||
1908 | $opts{justification} = 'left' | ||||||
1909 | if (exists $opts{justification} and $opts{justification} eq 'fill' and | ||||||
1910 | $self->get_property('map-fill-to-left')); | ||||||
1911 | my $tag = $self->{Text}->get_buffer->get_tag_table->lookup($name); | ||||||
1912 | $tag = $self->{Text}->get_buffer->create_tag($name, %opts) | ||||||
1913 | if not defined $tag; | ||||||
1914 | $tag->{WYSIWYG} = undef; # Use this later to store data? | ||||||
1915 | return $tag; | ||||||
1916 | } | ||||||
1917 | |||||||
1918 | sub _apply_tag_cascade { | ||||||
1919 | my $self = shift; | ||||||
1920 | my ($tag, $start, $end) = @_; | ||||||
1921 | my $buf = $self->{Text}->get_buffer; | ||||||
1922 | $tag = $self->{Text}->get_buffer->get_tag_table->lookup($tag) | ||||||
1923 | if not ref($tag); | ||||||
1924 | return if not defined $tag; | ||||||
1925 | my $regname = $self->_short_tag_name($tag); | ||||||
1926 | my $tdef = $TAGS{$regname}; | ||||||
1927 | if ($regname eq 'asis') { | ||||||
1928 | # Remove all non-paragraph tags | ||||||
1929 | $buf->get_tag_table-> | ||||||
1930 | foreach(sub { | ||||||
1931 | my ($tag) = @_; | ||||||
1932 | return if not $self->_is_my_tag($tag); | ||||||
1933 | my $name = $self->_short_tag_name($tag); | ||||||
1934 | return if (not exists $TAGS{$name} or | ||||||
1935 | $TAGS{$name}{Class} eq 'paragraph'); | ||||||
1936 | $self->_remove_tag($tag, $start, $end); | ||||||
1937 | }); | ||||||
1938 | $self->_apply_tag($tag, $start, $end); | ||||||
1939 | return 1; | ||||||
1940 | } | ||||||
1941 | if ($tdef->{Multi} or defined($tdef->{Group})) { | ||||||
1942 | $buf->get_tag_table-> | ||||||
1943 | foreach(sub { | ||||||
1944 | my ($tag) = @_; | ||||||
1945 | return if not $self->_is_my_tag($tag); | ||||||
1946 | my $name = $self->_short_tag_name($tag); | ||||||
1947 | $self->_remove_tag($tag, $start, $end) | ||||||
1948 | if (($tdef->{Multi} and $name eq $regname) or | ||||||
1949 | grep {$_ eq $name} @{$tdef->{Group}}); | ||||||
1950 | }); | ||||||
1951 | } | ||||||
1952 | if ($tdef->{Class} eq 'paragraph') { | ||||||
1953 | $self->_apply_tag($tag, $start, $end); | ||||||
1954 | return 1; | ||||||
1955 | } | ||||||
1956 | # Only apply this tag to places where the asis tag is not | ||||||
1957 | my $s = $start->copy; | ||||||
1958 | my $aname = $self->_full_tag_name('asis'); | ||||||
1959 | # my $asis = $buf->get_tag_table->lookup($aname); | ||||||
1960 | my $asis = $self->_create_tag($aname, %{$TAGS{asis}{Look}}); | ||||||
1961 | die("Gtk2::Ex::WYSIWYG tag naming conflict for $aname - " . | ||||||
1962 | "tag name already in use!") if not $self->_is_my_tag($asis); | ||||||
1963 | while (1) { | ||||||
1964 | my $asishere = 0; | ||||||
1965 | for my $tag ($s->get_tags) { | ||||||
1966 | next if $tag ne $asis; | ||||||
1967 | $asishere = 1; | ||||||
1968 | last; | ||||||
1969 | } | ||||||
1970 | $s->forward_to_tag_toggle($asis) if $asishere; | ||||||
1971 | return 1 if $s->compare($end) != -1; | ||||||
1972 | my $e = $s->copy; | ||||||
1973 | $e->forward_to_tag_toggle($asis); | ||||||
1974 | $e = $end->copy if $e->compare($end) == 1; | ||||||
1975 | # s to e is asis free | ||||||
1976 | $self->_apply_tag($tag, $start, $end); | ||||||
1977 | last if $e->equal($end); | ||||||
1978 | $s = $e; | ||||||
1979 | } | ||||||
1980 | return 1; | ||||||
1981 | } | ||||||
1982 | |||||||
1983 | sub _apply_tag { | ||||||
1984 | my $self = shift; | ||||||
1985 | my ($tag, $start, $end) = @_; | ||||||
1986 | $tag = $self->{Text}->get_buffer->get_tag_table->lookup($tag) | ||||||
1987 | if not ref $tag; | ||||||
1988 | $self->{Text}->get_buffer->apply_tag($tag, $start, $end) if defined $tag; | ||||||
1989 | } | ||||||
1990 | |||||||
1991 | sub _remove_tag_cascade { | ||||||
1992 | my $self = shift; | ||||||
1993 | my ($tag, $start, $end) = @_; | ||||||
1994 | # ONLY REMOVE THE TAG FROM THE AREAS IT IS APPLIED! | ||||||
1995 | my $buf = $self->{Text}->get_buffer; | ||||||
1996 | $self->_remove_tag($tag, $start, $end); | ||||||
1997 | $tag = $tag->get_property('name') if ref($tag); | ||||||
1998 | delete($self->{Active}{$tag}); | ||||||
1999 | return 1; | ||||||
2000 | } | ||||||
2001 | |||||||
2002 | sub _remove_tag { | ||||||
2003 | my $self = shift; | ||||||
2004 | my ($tag, $s, $e) = @_; | ||||||
2005 | my $buf = $self->{Text}->get_buffer; | ||||||
2006 | $tag = $buf->get_tag_table->lookup($tag) if not ref($tag); | ||||||
2007 | return if not defined $tag; | ||||||
2008 | my $t = $s->copy; | ||||||
2009 | SEARCH: while (1) { | ||||||
2010 | last if $t->compare($e) != -1; | ||||||
2011 | for my $ctag ($t->get_tags) { | ||||||
2012 | next if $ctag ne $tag; | ||||||
2013 | my $u = $t->copy; | ||||||
2014 | $t = $e->copy if (not $t->forward_to_tag_toggle($tag) or | ||||||
2015 | $t->compare($e) == 1); | ||||||
2016 | $buf->remove_tag($tag, $u, $t); | ||||||
2017 | next SEARCH; | ||||||
2018 | } | ||||||
2019 | last if not $t->forward_to_tag_toggle($tag); | ||||||
2020 | } | ||||||
2021 | } | ||||||
2022 | |||||||
2023 | # Given a tag name, ensure it is a tag controlled by this package. | ||||||
2024 | # Of course, if someone tries hard enough, this can be fooled | ||||||
2025 | sub _is_my_tag { | ||||||
2026 | my $self = shift; | ||||||
2027 | my ($tag) = @_; | ||||||
2028 | return 0 if not defined $tag or not exists $tag->{WYSIWYG}; | ||||||
2029 | return 1; | ||||||
2030 | } | ||||||
2031 | |||||||
2032 | sub _full_tag_name { | ||||||
2033 | my $self = shift; | ||||||
2034 | my ($name, @args) = @_; | ||||||
2035 | return $name->get_property('name') if ref($name); | ||||||
2036 | my $full = "gtkwysiwyg:$name"; | ||||||
2037 | $full .= ":" . join(":", @args) if scalar(@args); | ||||||
2038 | return $full; | ||||||
2039 | } | ||||||
2040 | |||||||
2041 | sub _short_tag_name { | ||||||
2042 | my $self = shift; | ||||||
2043 | my ($tag) = @_; | ||||||
2044 | $tag = $tag->get_property('name') if ref $tag; | ||||||
2045 | return undef if index($tag, 'gtkwysiwyg:') != 0; | ||||||
2046 | my $end = index($tag, ':', 11); | ||||||
2047 | return substr($tag, 11) if $end == -1; | ||||||
2048 | return substr($tag, 11, $end - 11); | ||||||
2049 | } | ||||||
2050 | |||||||
2051 | sub _tag_args { | ||||||
2052 | my $self = shift; | ||||||
2053 | my ($tag, $acnt) = @_; | ||||||
2054 | $tag = $tag->get_property('name') if ref($tag); | ||||||
2055 | return () if index($tag, 'gtkwysiwyg:') != 0; | ||||||
2056 | my $end = index($tag, ':', 11); | ||||||
2057 | return () if $end == -1; | ||||||
2058 | return (split(':', substr($tag, $end + 1), $acnt)); | ||||||
2059 | } | ||||||
2060 | |||||||
2061 | sub _tag_name_args { | ||||||
2062 | my $self = shift; | ||||||
2063 | my ($tag, $acnt) = @_; | ||||||
2064 | $tag = $tag->get_property('name') if ref($tag); | ||||||
2065 | return undef if index($tag, 'gtkwysiwyg:') != 0; | ||||||
2066 | my $end = index($tag, ':', 11); | ||||||
2067 | return substr($tag, 11) if $end == -1; | ||||||
2068 | return (substr($tag, 11, $end - 11), | ||||||
2069 | split(':', substr($tag, $end + 1), $acnt)); | ||||||
2070 | } | ||||||
2071 | |||||||
2072 | # Button/active manipulation | ||||||
2073 | |||||||
2074 | sub _set_active_from_text { | ||||||
2075 | # Set the active hash from the current position | ||||||
2076 | # Also keep track of whether the font and size should be set/'inconsistant' | ||||||
2077 | my $self = shift; | ||||||
2078 | $self->{Active} = {}; | ||||||
2079 | my $buf = $self->{Text}->get_buffer; | ||||||
2080 | my ($s, $e) = $buf->get_selection_bounds; | ||||||
2081 | if (not defined($s)) { | ||||||
2082 | ($s, $e) = $buf->get_bounds; | ||||||
2083 | return 0 if $s->equal($e); | ||||||
2084 | $s = $buf->get_iter_at_mark($buf->get_insert); | ||||||
2085 | $e = undef; | ||||||
2086 | } | ||||||
2087 | if (not defined($e)) { | ||||||
2088 | # No selection - also means only one possible font/size | ||||||
2089 | $s->backward_char if $s->compare($buf->get_start_iter) != 0; | ||||||
2090 | for my $tag ($s->get_tags) { | ||||||
2091 | next if not $self->_is_my_tag($tag); | ||||||
2092 | $self->{Active}{$tag->get_property('name')} = undef; | ||||||
2093 | } | ||||||
2094 | $self->{FontSet} = 1; | ||||||
2095 | $self->{SizeSet} = 1; | ||||||
2096 | } else { | ||||||
2097 | # Selection | ||||||
2098 | my $p = $s->copy; | ||||||
2099 | my $common = {}; | ||||||
2100 | my $fonts = {}; | ||||||
2101 | my $sizes = {}; | ||||||
2102 | while (1) { | ||||||
2103 | last if $p->compare($e) != -1; | ||||||
2104 | my $this = {}; | ||||||
2105 | my ($nofont, $nosize) = (1, 1); | ||||||
2106 | for my $tag ($p->get_tags) { | ||||||
2107 | next if not $self->_is_my_tag($tag); | ||||||
2108 | my $name = $self->_short_tag_name($tag); | ||||||
2109 | if ($name eq 'font') { | ||||||
2110 | $nofont = 0; | ||||||
2111 | my ($font) = $self->_tag_args($tag, 1); | ||||||
2112 | $fonts->{$font} = undef; | ||||||
2113 | } elsif ($name eq 'size') { | ||||||
2114 | $nosize = 0; | ||||||
2115 | my ($size) = $self->_tag_args($tag, 1); | ||||||
2116 | $sizes->{$size} = undef; | ||||||
2117 | } | ||||||
2118 | $name = $self->_full_tag_name($tag); | ||||||
2119 | $common->{$name} = undef if $p->equal($s); | ||||||
2120 | $this->{$name} = undef; | ||||||
2121 | } | ||||||
2122 | $fonts->{DEFAULT} = undef if $nofont; | ||||||
2123 | $sizes->{DEFAULT} = undef if $nosize; | ||||||
2124 | if (not $p->equal($s)) { | ||||||
2125 | for my $k (keys %$common) { | ||||||
2126 | delete($common->{$k}) if not exists $this->{$k}; | ||||||
2127 | } | ||||||
2128 | } | ||||||
2129 | last if not $p->forward_to_tag_toggle(undef); | ||||||
2130 | } | ||||||
2131 | $self->{Active} = $common; | ||||||
2132 | $self->{FontSet} = scalar(keys %$fonts) <= 1; | ||||||
2133 | $self->{SizeSet} = scalar(keys %$sizes) <= 1; | ||||||
2134 | } | ||||||
2135 | return 0; | ||||||
2136 | } | ||||||
2137 | |||||||
2138 | sub _set_buttons_from_active { | ||||||
2139 | my $self = shift; | ||||||
2140 | ++$self->{Lock}{Buttons}; | ||||||
2141 | # Font disabled if asis, enabled otherwise | ||||||
2142 | # size disabled if asis, enabled otherwise | ||||||
2143 | # size+/- disabled if asis, enabled otherwise | ||||||
2144 | # bold/italic/underline/strike/sup/sub/pre disabled if asis, enabled other | ||||||
2145 | for my $bname (keys %BUTTONS) { | ||||||
2146 | if ($bname eq 'Undo') { | ||||||
2147 | $self->{Buttons}{Undo}->set_sensitive(scalar(@{$self->{UndoStack}})); | ||||||
2148 | next; | ||||||
2149 | } elsif ($bname eq 'Redo') { | ||||||
2150 | $self->{Buttons}{Redo}->set_sensitive(scalar(@{$self->{RedoStack}})); | ||||||
2151 | next; | ||||||
2152 | } elsif (exists $self->{Active}{$self->_full_tag_name('asis')} and | ||||||
2153 | exists $BUTTONS{$bname}{Tag} and | ||||||
2154 | $BUTTONS{$bname}{Tag} ne 'asis' and | ||||||
2155 | $BUTTONS{$bname}{Tag} ne 'clear' and | ||||||
2156 | $TAGS{$BUTTONS{$bname}{Tag}}{Class} eq 'font') { | ||||||
2157 | $self->{Buttons}{$bname}->set_sensitive(0); | ||||||
2158 | } else { | ||||||
2159 | $self->{Buttons}{$bname}->set_sensitive(1); | ||||||
2160 | } | ||||||
2161 | next if $BUTTONS{$bname}{Type} eq 'button'; | ||||||
2162 | if ($BUTTONS{$bname}{Type} eq 'menu') { | ||||||
2163 | $self->{Buttons}{$bname}-> | ||||||
2164 | set_text($self->_get_current_menu_state($bname)); | ||||||
2165 | } elsif ($BUTTONS{$bname}{Type} eq 'font') { | ||||||
2166 | if ($self->{FontSet}) { | ||||||
2167 | $self->{Buttons}{$bname}-> | ||||||
2168 | set_text($self->_get_current_font_state($bname)); | ||||||
2169 | } else { | ||||||
2170 | $self->{Buttons}{$bname}->set_inconsistant; | ||||||
2171 | } | ||||||
2172 | } elsif ($BUTTONS{$bname}{Type} eq 'size') { | ||||||
2173 | if ($self->{SizeSet}) { | ||||||
2174 | $self->{Buttons}{$bname}->set_value($self->_get_current_size($bname)); | ||||||
2175 | } else { | ||||||
2176 | $self->{Buttons}{$bname}->set_inconsistant; | ||||||
2177 | } | ||||||
2178 | } elsif ($BUTTONS{$bname}{Type} eq 'toggle') { | ||||||
2179 | $self->{Buttons}{$bname}-> | ||||||
2180 | set_active($self->_get_current_toggle_state($bname)); | ||||||
2181 | } | ||||||
2182 | } | ||||||
2183 | --$self->{Lock}{Buttons}; | ||||||
2184 | return 0; | ||||||
2185 | } | ||||||
2186 | |||||||
2187 | sub _get_current_toggle_state { | ||||||
2188 | my $self = shift; | ||||||
2189 | my ($bname) = @_; | ||||||
2190 | my $tag = $BUTTONS{$bname}{Tag}; | ||||||
2191 | if ($TAGS{$tag}{Multi}) { | ||||||
2192 | for my $k (keys %{$self->{Active}}) { | ||||||
2193 | next if $self->_short_tag_name($k) ne $tag; | ||||||
2194 | return 1; | ||||||
2195 | } | ||||||
2196 | } elsif (exists($self->{Active}{$self->_full_tag_name($tag)})) { | ||||||
2197 | return 1; | ||||||
2198 | } | ||||||
2199 | return 0 if not exists $TAGS{$tag}{Default}; | ||||||
2200 | if ($TAGS{$tag}{Default} eq $tag) { | ||||||
2201 | for my $other (@{$TAGS{$tag}{Group}}) { | ||||||
2202 | return 0 if exists($self->{Active}{$self->_full_tag_name($other)}); | ||||||
2203 | } | ||||||
2204 | return 1; | ||||||
2205 | } | ||||||
2206 | return 0; | ||||||
2207 | } | ||||||
2208 | |||||||
2209 | sub _get_current_menu_state { | ||||||
2210 | my $self = shift; | ||||||
2211 | my ($bname) = @_; | ||||||
2212 | for my $tdef (@{$BUTTONS{$bname}{Tags}}) { | ||||||
2213 | my ($tagname, $display) = @$tdef; | ||||||
2214 | next if not exists $self->{Active}{$self->_full_tag_name($tagname)}; | ||||||
2215 | return $display; | ||||||
2216 | } | ||||||
2217 | return $BUTTONS{$bname}{Default}; | ||||||
2218 | } | ||||||
2219 | |||||||
2220 | sub _get_current_font_state { | ||||||
2221 | my $self = shift; | ||||||
2222 | my ($bname) = @_; | ||||||
2223 | for my $fname (@{$BUTTONS{$bname}{Tags}}) { | ||||||
2224 | next if not exists $self->{Active}{$self->_full_tag_name('font', | ||||||
2225 | $fname)}; | ||||||
2226 | return $fname; | ||||||
2227 | } | ||||||
2228 | return $BUTTONS{$bname}{Default}; | ||||||
2229 | } | ||||||
2230 | |||||||
2231 | sub _get_current_size { | ||||||
2232 | my $self = shift; | ||||||
2233 | my ($bname) = @_; | ||||||
2234 | my $tname = $BUTTONS{$bname}{Tag}; | ||||||
2235 | for my $k (keys %{$self->{Active}}) { | ||||||
2236 | my ($name, $size) = $self->_tag_name_args($k); | ||||||
2237 | next if $name ne $tname; | ||||||
2238 | return $size; | ||||||
2239 | } | ||||||
2240 | return $BUTTONS{$bname}{Default}; | ||||||
2241 | } | ||||||
2242 | |||||||
2243 | # Paragraph normalisation | ||||||
2244 | |||||||
2245 | sub _normalise_paragraph { | ||||||
2246 | my $self = shift; | ||||||
2247 | my ($s, $e) = @_; | ||||||
2248 | my ($ps, $pe) = $self->_get_paragraph_bounds($s, $e); | ||||||
2249 | my $buf = $self->{Text}->get_buffer; | ||||||
2250 | my @apply; | ||||||
2251 | for my $tag ($ps->get_tags) { | ||||||
2252 | next if not $self->_is_my_tag($tag); | ||||||
2253 | my $name = $self->_short_tag_name($tag); | ||||||
2254 | push @apply, $tag if (exists($TAGS{$name}) and | ||||||
2255 | $TAGS{$name}{Class} eq 'paragraph'); | ||||||
2256 | } | ||||||
2257 | $buf->get_tag_table->foreach(sub { | ||||||
2258 | my ($tag) = @_; | ||||||
2259 | return if not $self->_is_my_tag($tag); | ||||||
2260 | my $name = $self->_short_tag_name($tag); | ||||||
2261 | $self->_remove_tag($tag, $ps, $pe) | ||||||
2262 | if (exists $TAGS{$name} and | ||||||
2263 | $TAGS{$name}{Class} eq 'paragraph'); | ||||||
2264 | }); | ||||||
2265 | for my $tag (@apply) { | ||||||
2266 | $self->_apply_tag_cascade($tag, $ps, $pe); | ||||||
2267 | } | ||||||
2268 | } | ||||||
2269 | |||||||
2270 | # Bounds fetching | ||||||
2271 | |||||||
2272 | sub _get_current_bounds_for_tag { | ||||||
2273 | my $self = shift; | ||||||
2274 | my ($tname) = @_; | ||||||
2275 | if ($TAGS{$tname}{Class} eq 'paragraph') { | ||||||
2276 | return $self->_get_current_paragraph_bounds; | ||||||
2277 | } else { | ||||||
2278 | my $buf = $self->{Text}->get_buffer; | ||||||
2279 | my ($s, $e) = $buf->get_selection_bounds; | ||||||
2280 | if (not defined($s)) { | ||||||
2281 | $s = $buf->get_iter_at_mark($buf->get_insert); | ||||||
2282 | $e = $s->copy; | ||||||
2283 | } | ||||||
2284 | return ($s, $e); | ||||||
2285 | } | ||||||
2286 | } | ||||||
2287 | |||||||
2288 | sub _get_current_paragraph_bounds { | ||||||
2289 | my $self = shift; | ||||||
2290 | my $buf = $self->{Text}->get_buffer; | ||||||
2291 | my ($s, $e) = $buf->get_selection_bounds; | ||||||
2292 | if (not defined($s)) { | ||||||
2293 | $s = $buf->get_iter_at_mark($buf->get_insert); | ||||||
2294 | $e = $s->copy; | ||||||
2295 | } | ||||||
2296 | return $self->_get_paragraph_bounds($s, $e); | ||||||
2297 | } | ||||||
2298 | |||||||
2299 | sub _get_paragraph_bounds { | ||||||
2300 | my $self = shift; | ||||||
2301 | my ($s, $e) = @_; | ||||||
2302 | my ($ps, $pe); | ||||||
2303 | if ($self->_iter_in_real_paragraph($s)) { | ||||||
2304 | ($ps, $pe) = $self->_get_real_paragraph_bounds_for_iter($s); | ||||||
2305 | } else { | ||||||
2306 | ($ps, $pe) = $self->_get_inter_paragraph_bounds_for_iter($s); | ||||||
2307 | } | ||||||
2308 | return ($ps, $pe) if ($s->equal($e) or $e->compare($pe) == -1); | ||||||
2309 | if ($self->_iter_in_real_paragraph($e)) { | ||||||
2310 | (my $t, $pe) = $self->_get_real_paragraph_bounds_for_iter($e); | ||||||
2311 | } else { | ||||||
2312 | (my $t, $pe) = $self->_get_real_paragraph_bounds_for_iter($e); | ||||||
2313 | } | ||||||
2314 | return ($ps, $pe); | ||||||
2315 | } | ||||||
2316 | |||||||
2317 | sub _iter_in_real_paragraph { | ||||||
2318 | ## ASIS AND PRE TAGS! | ||||||
2319 | ## newlines inside pre/asis tags do not count as 'paragraph breakers' | ||||||
2320 | ## In fact, _ANYTHING_ inside pre/asis tags count as a single 'non-space' | ||||||
2321 | ## item | ||||||
2322 | ## A\n\nB -> paragraphs are A and B | ||||||
2323 | ## A \n\n B -> all one paragraph |
||||||
2324 | ## A\n\n \n\n\n\n \n\nB => paragraphs are A,\n\n\n\n and B |
||||||
2325 | my $self = shift; | ||||||
2326 | my ($i) = @_; | ||||||
2327 | return 1 if not $self->_get_newline_state_at_iter($i); | ||||||
2328 | my $j = $i->copy; | ||||||
2329 | $j->forward_char; | ||||||
2330 | my $curr = $i->get_slice($j); | ||||||
2331 | return 1 if $curr =~ /\S/; | ||||||
2332 | my $prenl = 0; | ||||||
2333 | my $postnl = 0; | ||||||
2334 | ++$postnl if $curr eq "\n"; | ||||||
2335 | my $FOUNDNL = 0; | ||||||
2336 | my $lookfor = sub { | ||||||
2337 | $FOUNDNL = ($_[0] eq "\n"); | ||||||
2338 | return (($_[0] eq "\n") or ($_[0] =~ /\S/)); | ||||||
2339 | }; | ||||||
2340 | my $s = $i->copy; | ||||||
2341 | while ($s->backward_find_char($lookfor)) { | ||||||
2342 | last if not $FOUNDNL or not $self->_get_newline_state_at_iter($s); | ||||||
2343 | last if ++$prenl == 2; | ||||||
2344 | } | ||||||
2345 | return 1 if $prenl == 0; | ||||||
2346 | my $e = $i->copy; | ||||||
2347 | while ($e->forward_find_char($lookfor)) { | ||||||
2348 | last if not $FOUNDNL or not $self->_get_newline_state_at_iter($e); | ||||||
2349 | last if ++$postnl == 2; | ||||||
2350 | } | ||||||
2351 | return $postnl == 0; | ||||||
2352 | } | ||||||
2353 | |||||||
2354 | sub _get_real_paragraph_bounds_for_iter { | ||||||
2355 | my $self = shift; | ||||||
2356 | my ($i) = @_; | ||||||
2357 | my $s = $i->copy; | ||||||
2358 | my $e = $i->copy; | ||||||
2359 | $e->forward_char; | ||||||
2360 | my $curr = $s->get_slice($e); | ||||||
2361 | my $lastnl = undef; | ||||||
2362 | my $FOUNDNL = 0; | ||||||
2363 | my $lookfor = sub { | ||||||
2364 | $FOUNDNL = ($_[0] eq "\n"); | ||||||
2365 | return (($_[0] eq "\n") or ($_[0] =~ /\S/)); | ||||||
2366 | }; | ||||||
2367 | while (1) { | ||||||
2368 | if (not $s->backward_find_char($lookfor)) { | ||||||
2369 | $s = $self->{Text}->get_buffer->get_start_iter; | ||||||
2370 | last; | ||||||
2371 | } elsif ($FOUNDNL) { | ||||||
2372 | # If this NL is in pre or asis, it counts as a \S | ||||||
2373 | if (not $self->_get_newline_state_at_iter($s)) { | ||||||
2374 | $lastnl = undef; # lastnl is invalidated when we find \S | ||||||
2375 | next; | ||||||
2376 | } elsif (defined($lastnl)) { | ||||||
2377 | $s = $lastnl; | ||||||
2378 | $s->forward_char; | ||||||
2379 | last; | ||||||
2380 | } | ||||||
2381 | $lastnl = $s->copy; | ||||||
2382 | } else { | ||||||
2383 | # Found a \S -> lastnl is invalidated | ||||||
2384 | $lastnl = undef; | ||||||
2385 | } | ||||||
2386 | } | ||||||
2387 | # Found new start, now find new end | ||||||
2388 | $e = $i->copy; | ||||||
2389 | $lastnl = undef; | ||||||
2390 | $lastnl = $i->copy if ($curr eq "\n" and | ||||||
2391 | $self->_get_newline_state_at_iter($e)); | ||||||
2392 | while (1) { | ||||||
2393 | if (not $e->forward_find_char($lookfor)) { | ||||||
2394 | $e = $self->{Text}->get_buffer->get_end_iter; | ||||||
2395 | last; | ||||||
2396 | } elsif ($FOUNDNL) { | ||||||
2397 | if (not $self->_get_newline_state_at_iter($s)) { | ||||||
2398 | $lastnl = undef; | ||||||
2399 | next; | ||||||
2400 | } elsif (defined($lastnl)) { | ||||||
2401 | $e = $lastnl; | ||||||
2402 | last; | ||||||
2403 | } | ||||||
2404 | $lastnl = $e->copy; | ||||||
2405 | next; | ||||||
2406 | } | ||||||
2407 | $lastnl = undef; | ||||||
2408 | } | ||||||
2409 | return ($s, $e); | ||||||
2410 | } | ||||||
2411 | |||||||
2412 | # _get_newline_state_at_iter - true -> raw newline, can be used for paragraph | ||||||
2413 | # searching, false -> 'asis' newline, cannot be used for paragraph searching | ||||||
2414 | sub _get_newline_state_at_iter { | ||||||
2415 | my $self = shift; | ||||||
2416 | my ($i) = @_; | ||||||
2417 | for my $tag ($i->get_tags) { | ||||||
2418 | next if not $self->_is_my_tag($tag); | ||||||
2419 | my $name = $self->_short_tag_name($tag); | ||||||
2420 | return 0 if $name eq 'asis' or $name eq 'pre'; | ||||||
2421 | } | ||||||
2422 | return 1; | ||||||
2423 | } | ||||||
2424 | |||||||
2425 | sub _get_inter_paragraph_bounds_for_iter { | ||||||
2426 | my $self = shift; | ||||||
2427 | my ($i) = @_; | ||||||
2428 | my $s = $i->copy; | ||||||
2429 | my $e = $i->copy; | ||||||
2430 | $e->forward_char; | ||||||
2431 | my $curr = $s->get_slice($e); | ||||||
2432 | my $lastnl = ($curr eq "\n" ? $s->copy : undef); | ||||||
2433 | my $FOUNDNL = 0; | ||||||
2434 | my $lookfor = sub { | ||||||
2435 | $FOUNDNL = ($_[0] eq "\n"); | ||||||
2436 | return (($_[0] eq "\n") or ($_[0] =~ /\S/)); | ||||||
2437 | }; | ||||||
2438 | while (1) { | ||||||
2439 | if (not $s->backward_find_char($lookfor)) { | ||||||
2440 | if (not defined($lastnl)) { | ||||||
2441 | $s = $self->{Text}->get_buffer->get_start_iter; | ||||||
2442 | } else { | ||||||
2443 | $s = $lastnl; | ||||||
2444 | $s->forward_char; | ||||||
2445 | } | ||||||
2446 | } elsif ($FOUNDNL) { | ||||||
2447 | if (not $self->_get_newline_state_at_iter($s)) { | ||||||
2448 | # counts as \S! | ||||||
2449 | die "Invalid use of _get_inter_paragraph_bounds_for_iter" | ||||||
2450 | if not defined $lastnl; | ||||||
2451 | $s = $lastnl; | ||||||
2452 | $s->forward_char; | ||||||
2453 | } else { | ||||||
2454 | $lastnl = $s->copy; | ||||||
2455 | next; | ||||||
2456 | } | ||||||
2457 | } else { # Found a \S! | ||||||
2458 | die "Invalid use of _get_inter_paragraph_bounds_for_iter" | ||||||
2459 | if not defined $lastnl; | ||||||
2460 | $s = $lastnl; | ||||||
2461 | $s->forward_char; | ||||||
2462 | } | ||||||
2463 | last; | ||||||
2464 | } | ||||||
2465 | $lastnl = ($curr eq "\n" ? $i->copy : undef); | ||||||
2466 | $e = $i->copy; | ||||||
2467 | while (1) { | ||||||
2468 | if (not $e->forward_find_char($lookfor)) { | ||||||
2469 | if (not defined($lastnl)) { | ||||||
2470 | $e = $self->{Text}->get_buffer->get_end_iter; | ||||||
2471 | } else { | ||||||
2472 | $e = $lastnl; | ||||||
2473 | $e->forward_char; | ||||||
2474 | } | ||||||
2475 | } elsif ($FOUNDNL) { | ||||||
2476 | if (not $self->_get_newline_state_at_iter($e)) { | ||||||
2477 | # Counts as \S! | ||||||
2478 | die "Invalid use of _get_inter_paragraph_bounds_for_iter" | ||||||
2479 | if not defined $lastnl; | ||||||
2480 | $e = $lastnl; | ||||||
2481 | $e->forward_char; | ||||||
2482 | } else { | ||||||
2483 | $lastnl = $e->copy; | ||||||
2484 | next; | ||||||
2485 | } | ||||||
2486 | } else { # Found a \S! | ||||||
2487 | die "Invalid use of _get_inter_paragraph_bounds_for_iter" | ||||||
2488 | if not defined $lastnl; | ||||||
2489 | $e = $lastnl; | ||||||
2490 | $e->forward_char; | ||||||
2491 | } | ||||||
2492 | last; | ||||||
2493 | } | ||||||
2494 | return ($s, $e); | ||||||
2495 | } | ||||||
2496 | |||||||
2497 | sub _merge_tags { | ||||||
2498 | my $self = shift; | ||||||
2499 | my ($user, $auto) = @_; | ||||||
2500 | # AUTO overrides USER tags | ||||||
2501 | my @stack; | ||||||
2502 | my $ui = 0; | ||||||
2503 | my $ustart = undef; | ||||||
2504 | for my $ai (0..(scalar(@$auto) - 1)) { | ||||||
2505 | if ($ui >= scalar(@$user)) { | ||||||
2506 | push @stack, $auto->[$ai]; | ||||||
2507 | next; | ||||||
2508 | } | ||||||
2509 | my $start = (defined($ustart) ? $ustart : $user->[$ui]{Start}); | ||||||
2510 | while ($ui < scalar(@$user) and $user->[$ui]{End} <= $auto->[$ai]{Start}) { | ||||||
2511 | push @stack, {Start => $start, | ||||||
2512 | End => $user->[$ui]{End}, | ||||||
2513 | Tags => {%{$user->[$ui]{Tags}}}}; | ||||||
2514 | $ustart = undef; | ||||||
2515 | ++$ui; | ||||||
2516 | $start = ($ui < scalar(@$user) ? $user->[$ui]{Start} : undef); | ||||||
2517 | } | ||||||
2518 | if ($ui >= scalar(@$user)) { | ||||||
2519 | push @stack, $auto->[$ai]; | ||||||
2520 | next; | ||||||
2521 | } | ||||||
2522 | if ($start >= $auto->[$ai]{End}) { | ||||||
2523 | push @stack, $auto->[$ai]; | ||||||
2524 | next; | ||||||
2525 | } | ||||||
2526 | if ($start < $auto->[$ai]{Start}) { | ||||||
2527 | push @stack, {Start => $start, | ||||||
2528 | End => $auto->[$ai]{Start}, | ||||||
2529 | Tags => {%{$user->[$ui]{Tags}}}}; | ||||||
2530 | } | ||||||
2531 | $ustart = $auto->[$ai]{End}; | ||||||
2532 | if ($ustart >= $user->[$ui]{End}) { | ||||||
2533 | $ustart = undef; | ||||||
2534 | ++$ui; | ||||||
2535 | } | ||||||
2536 | push @stack, $auto->[$ai]; | ||||||
2537 | } | ||||||
2538 | for my $i ($ui..(scalar(@$user) - 1)) { | ||||||
2539 | if (defined($ustart)) { | ||||||
2540 | push @stack, {Start => $ustart, | ||||||
2541 | End => $user->[$i]{End}, | ||||||
2542 | Tags => {%{$user->[$i]{Tags}}}}; | ||||||
2543 | $ustart = undef; | ||||||
2544 | } else { | ||||||
2545 | push @stack, $user->[$i]; | ||||||
2546 | } | ||||||
2547 | } | ||||||
2548 | return @stack; | ||||||
2549 | } | ||||||
2550 | |||||||
2551 | sub _get_auto_tags { | ||||||
2552 | my $self = shift; | ||||||
2553 | my ($s, $e) = $self->{Text}->get_buffer->get_bounds; | ||||||
2554 | my @stack = (); | ||||||
2555 | my ($FOUNDNL, $FOUNDWS, $SAWS) = (0, 0, 0); | ||||||
2556 | my $find = sub { | ||||||
2557 | $FOUNDNL = $_[0] eq "\n"; | ||||||
2558 | $FOUNDWS = $_[0] =~ /\s/; | ||||||
2559 | $SAWS = 1 if not $SAWS and $_[0] =~ /\S/; | ||||||
2560 | return ($FOUNDNL or $FOUNDWS); | ||||||
2561 | }; | ||||||
2562 | my $lastnl = undef; | ||||||
2563 | my $pstart = undef; | ||||||
2564 | my $wsstart = undef; | ||||||
2565 | my $lastws = undef; | ||||||
2566 | while (1) { | ||||||
2567 | ($FOUNDNL, $FOUNDWS, $SAWS) = (0, 0, 0); | ||||||
2568 | last if $s->equal($e) or not $s->forward_find_char($find); | ||||||
2569 | if (not $self->_get_newline_state_at_iter($s)) { | ||||||
2570 | # This isn't really whitespace or a newline, so process open tags | ||||||
2571 | if (defined($pstart)) { | ||||||
2572 | push @stack, {Start => $pstart, | ||||||
2573 | End => $SAWS ? $lastnl : $s->get_offset, | ||||||
2574 | Tags => {p => undef}}; | ||||||
2575 | } elsif (defined($lastnl)) { | ||||||
2576 | push @stack, {Start => $lastnl, | ||||||
2577 | End => $lastnl + 1, | ||||||
2578 | Tags => {br => undef}}; | ||||||
2579 | } | ||||||
2580 | if (defined($wsstart) and $lastws - $wsstart > 0) { | ||||||
2581 | push @stack, {Start => $wsstart, | ||||||
2582 | End => $lastws + 1, | ||||||
2583 | Tags => {ws => undef}}; | ||||||
2584 | } | ||||||
2585 | ($pstart, $lastnl, $wsstart, $lastws) = (undef, undef, undef, undef); | ||||||
2586 | next; | ||||||
2587 | } | ||||||
2588 | # a nl or space here! | ||||||
2589 | if ($SAWS) { | ||||||
2590 | # We passed a \S, so handle any existing newlines/paras/ws | ||||||
2591 | if (defined($pstart)) { | ||||||
2592 | push @stack, {Start => $pstart, | ||||||
2593 | End => $lastnl + 1, | ||||||
2594 | Tags => {p => undef}}; | ||||||
2595 | } elsif (defined($lastnl)) { | ||||||
2596 | push @stack, {Start => $lastnl, | ||||||
2597 | End => $lastnl + 1, | ||||||
2598 | Tags => {br => undef}}; | ||||||
2599 | } | ||||||
2600 | if (defined($wsstart) and $lastws - $wsstart > 0) { | ||||||
2601 | push @stack, {Start => $wsstart, | ||||||
2602 | End => $lastws + 1, | ||||||
2603 | Tags => {ws => undef}}; | ||||||
2604 | } | ||||||
2605 | ($pstart, $lastnl, $wsstart, $lastws) = (undef, undef, undef, undef); | ||||||
2606 | } | ||||||
2607 | if ($FOUNDNL) { | ||||||
2608 | if (defined($pstart)) { | ||||||
2609 | # Continuing a paragraph | ||||||
2610 | $lastnl = $s->get_offset; | ||||||
2611 | next; | ||||||
2612 | } elsif (defined($lastnl)) { | ||||||
2613 | # New paragraph break! | ||||||
2614 | $pstart = $lastnl; | ||||||
2615 | $lastnl = $s->get_offset; | ||||||
2616 | } else { | ||||||
2617 | # Found a newline! | ||||||
2618 | $lastnl = $s->get_offset; | ||||||
2619 | } | ||||||
2620 | if (defined($wsstart) and $lastws - $wsstart > 0) { | ||||||
2621 | push @stack, {Start => $wsstart, | ||||||
2622 | End => $lastws + 1, | ||||||
2623 | Tags => {ws => undef}}; | ||||||
2624 | } | ||||||
2625 | ($wsstart, $lastws) = (undef, undef); | ||||||
2626 | # WS to process? | ||||||
2627 | } elsif ($FOUNDWS) { | ||||||
2628 | if (defined($wsstart)) { | ||||||
2629 | $lastws = $s->get_offset; | ||||||
2630 | } else { | ||||||
2631 | $wsstart = $lastws = $s->get_offset; | ||||||
2632 | } | ||||||
2633 | } | ||||||
2634 | } | ||||||
2635 | # anything left open? | ||||||
2636 | if (defined($pstart)) { | ||||||
2637 | push @stack, {Start => $pstart, | ||||||
2638 | End => $lastnl + 1, | ||||||
2639 | Tags => {p => undef}}; | ||||||
2640 | } elsif (defined($lastnl)) { | ||||||
2641 | push @stack, {Start => $lastnl, | ||||||
2642 | End => $lastnl + 1, | ||||||
2643 | Tags => {br => undef}}; | ||||||
2644 | } | ||||||
2645 | if (defined($wsstart) and $lastws - $wsstart > 0) { | ||||||
2646 | push @stack, {Start => $wsstart, | ||||||
2647 | End => $lastws + 1, | ||||||
2648 | Tags => {ws => undef}}; | ||||||
2649 | } | ||||||
2650 | return @stack; | ||||||
2651 | } | ||||||
2652 | |||||||
2653 | sub _get_user_tags { | ||||||
2654 | my $self = shift; | ||||||
2655 | my ($s, $e) = $self->{Text}->get_buffer->get_bounds; | ||||||
2656 | my @stack = ({Start => undef, | ||||||
2657 | End => undef, | ||||||
2658 | Tags => {}}); | ||||||
2659 | while (1) { | ||||||
2660 | last if $s->equal($e); | ||||||
2661 | # This is the end of the previous tag group too | ||||||
2662 | if (defined($stack[-1]{Start})) { | ||||||
2663 | $stack[-1]{End} = $s->get_offset; | ||||||
2664 | push @stack, {Start => undef, | ||||||
2665 | End => undef, | ||||||
2666 | Tags => {}}; | ||||||
2667 | } | ||||||
2668 | for my $tag ($s->get_tags) { | ||||||
2669 | next if not $self->_is_my_tag($tag); | ||||||
2670 | my $name = $self->_short_tag_name($tag); | ||||||
2671 | next if not exists $TAGS{$name}; | ||||||
2672 | $stack[-1]{Start} = $s->get_offset if not defined $stack[-1]{Start}; | ||||||
2673 | my $val; | ||||||
2674 | if (exists $tag->{Target}) { | ||||||
2675 | $val = $tag->{Target}; | ||||||
2676 | } elsif ($TAGS{$name}{ArgumentCount} > 0) { | ||||||
2677 | $val = [$self->_tag_args($tag, $TAGS{$name}{ArgumentCount})]; | ||||||
2678 | } | ||||||
2679 | $stack[-1]{Tags}{$name} = $val; | ||||||
2680 | } | ||||||
2681 | last if not $s->forward_to_tag_toggle(undef); | ||||||
2682 | } | ||||||
2683 | if (defined($stack[-1]{Start})) { | ||||||
2684 | $stack[-1]{End} = $s->get_offset; | ||||||
2685 | } else { | ||||||
2686 | pop(@stack); | ||||||
2687 | } | ||||||
2688 | return @stack; | ||||||
2689 | } | ||||||
2690 | |||||||
2691 | sub _set_cursor { | ||||||
2692 | my $self = shift; | ||||||
2693 | my ($x, $y) = @_; | ||||||
2694 | ($x, $y) = $self->{Text}->window_to_buffer_coords('widget', | ||||||
2695 | $self->{Text}->get_pointer) | ||||||
2696 | if not defined $x; | ||||||
2697 | my $iter = $self->{Text}->get_iter_at_location($x, $y); | ||||||
2698 | return unless defined $iter; | ||||||
2699 | my ($target); | ||||||
2700 | for my $tag ($iter->get_tags) { | ||||||
2701 | next if not $self->_is_my_tag($tag) or not exists $tag->{Target}; | ||||||
2702 | $target = $tag->{Target}; | ||||||
2703 | last; | ||||||
2704 | } | ||||||
2705 | my $cursor = defined($target) ? 'Link': 'Text' ; | ||||||
2706 | if ($cursor ne $self->{Cursor}{Current}) { | ||||||
2707 | $self->{Cursor}{Current} = $cursor; | ||||||
2708 | $self->{Text}->get_window('text')->set_cursor($self->{Cursor}{$cursor}); | ||||||
2709 | if ($cursor eq 'Text') { | ||||||
2710 | $self->_tooltip_hide; | ||||||
2711 | } else { | ||||||
2712 | $self->_tooltip_text($target); | ||||||
2713 | $self->_tooltip_show($x, $y); | ||||||
2714 | } | ||||||
2715 | } elsif ($cursor eq 'Link' and $self->{CurrentLink} ne $target) { | ||||||
2716 | $self->_tooltip_hide; | ||||||
2717 | $self->_tooltip_text($target); | ||||||
2718 | $self->_tooltip_show($x, $y); | ||||||
2719 | } | ||||||
2720 | $self->{CurrentLink} = $target; | ||||||
2721 | } | ||||||
2722 | |||||||
2723 | # Tags. | ||||||
2724 | # Tags all have a simple name (just alphanumerics, no punctiation at all) which | ||||||
2725 | # is used as a key to the %TAGS hash. Each value is a hashref with the | ||||||
2726 | # following keys/values: | ||||||
2727 | # Class: either 'font' or 'paragraph'. Paragraph class tags affect an entire | ||||||
2728 | # paragraph, while font class tags only affect their immediate area | ||||||
2729 | # (be that the current selection or the currect active modes) | ||||||
2730 | # Look: the properties of the text tag for this tag. Not all tags will | ||||||
2731 | # equate to an actual text tag (the clear tag for instance just holds | ||||||
2732 | # code on what to do when the Clear button is hit), but any tag that | ||||||
2733 | # should apply a style to the text should have a Look key. | ||||||
2734 | # Multi: true or false, this indicates whether the tag is a definition that | ||||||
2735 | # is to be used to create text tags that are named with at least one | ||||||
2736 | # argument. This is for tags that can be applied incrementally, or | ||||||
2737 | # whose look depends on an argument (for example, indent and font | ||||||
2738 | # respectively). | ||||||
2739 | # ArgumentCount: Some tags have arguments (for instance, the size tag takes | ||||||
2740 | # the numeric size as an argument). For parsing and output | ||||||
2741 | # purposes, the number of those arguments must be kept in the | ||||||
2742 | # ArgumentCount key. If a tag has no arguments, this key | ||||||
2743 | # should not be present. | ||||||
2744 | # Group: For tags that belong to a group (of which only one should be applied | ||||||
2745 | # at a time), this key has an arrayref as a value, each element of | ||||||
2746 | # which is the name of the other tags in the group. | ||||||
2747 | # Default: For group tags, this specifies which tag should be turned on if | ||||||
2748 | # all the other tags are turned off. | ||||||
2749 | # Activate: For tags that are connected to non-toggle buttons, this key holds | ||||||
2750 | # a coderef to be run when the button is clicked. Arguments are | ||||||
2751 | # the WYSIWYG widget, the button name, and the start and end iters | ||||||
2752 | # of the affected area of text. | ||||||
2753 | # ToggleOn: For tags that are connected to toggle buttons and that are marked | ||||||
2754 | # as Multi, this key holds a coderef to be run when the button is | ||||||
2755 | # toggled to the ON position. Like Activate, the arguments are the | ||||||
2756 | # WYSIWYG widget, the button name and the start and end iters of | ||||||
2757 | # the affected area of text. | ||||||
2758 | # ToggleOff: As for ToggleOn, but called when the button is toggled to the | ||||||
2759 | # OFF position. | ||||||
2760 | BEGIN { | ||||||
2761 | %TAGS = (clear => | ||||||
2762 | # Fake tag to hold action for the 'Clear' buttons | ||||||
2763 | {Class => 'font', | ||||||
2764 | Activate => sub { | ||||||
2765 | my $self = shift; | ||||||
2766 | my ($bname, $s, $e) = @_; | ||||||
2767 | $self->_clear_font_formatting($s, $e); | ||||||
2768 | }}, | ||||||
2769 | |||||||
2770 | # 'bold' - makes the text bold. | ||||||
2771 | # Used by the 'Bold' button (a toggle) | ||||||
2772 | bold => {Class => 'font', | ||||||
2773 | Look => {weight => PANGO_WEIGHT_BOLD}}, | ||||||
2774 | |||||||
2775 | # 'italic' - makes the text italic. | ||||||
2776 | # Used by the 'Italic' button (a toggle) | ||||||
2777 | italic => {Class => 'font', | ||||||
2778 | Look => {style => 'italic'}}, | ||||||
2779 | |||||||
2780 | # 'underline' - makes the text underlined. | ||||||
2781 | # off asis. Used by the 'Underline' button (a toggle) | ||||||
2782 | underline => {Class => 'font', | ||||||
2783 | Look => {underline => 'single'}}, | ||||||
2784 | |||||||
2785 | # 'strikethrough' - makes the text struck. | ||||||
2786 | # off asis. Used by the 'Strike' button (a toggle) | ||||||
2787 | strikethrough => {Class => 'font', | ||||||
2788 | Look => {strikethrough => 1}}, | ||||||
2789 | |||||||
2790 | superscript => {Class => 'font', | ||||||
2791 | Multi => 1, | ||||||
2792 | ArgumentCount => 2, | ||||||
2793 | ToggleOn => sub { | ||||||
2794 | my $self = shift; | ||||||
2795 | my ($bname, $s, $e) = @_; | ||||||
2796 | $self->_superscript_on($s, $e); | ||||||
2797 | }, | ||||||
2798 | ToggleOff => sub { | ||||||
2799 | my $self = shift; | ||||||
2800 | my ($bname, $s, $e) = @_; | ||||||
2801 | $self->_superscript_off($s, $e); | ||||||
2802 | }}, | ||||||
2803 | |||||||
2804 | subscript => {Class => 'font', | ||||||
2805 | Multi => 1, | ||||||
2806 | ArgumentCount => 2, | ||||||
2807 | ToggleOn => sub { | ||||||
2808 | my $self = shift; | ||||||
2809 | my ($bname, $s, $e) = @_; | ||||||
2810 | $self->_subscript_on($s, $e); | ||||||
2811 | }, | ||||||
2812 | ToggleOff => sub { | ||||||
2813 | my $self = shift; | ||||||
2814 | my ($bname, $s, $e) = @_; | ||||||
2815 | $self->_subscript_off($s, $e); | ||||||
2816 | }}, | ||||||
2817 | |||||||
2818 | link => | ||||||
2819 | {Class => 'font', | ||||||
2820 | Multi => 1, # ie, create link_0, link_1 etc instead of link | ||||||
2821 | Look => {underline => 'single', | ||||||
2822 | foreground => 'blue'}, | ||||||
2823 | ToggleOn => sub { | ||||||
2824 | my $self = shift; | ||||||
2825 | my ($bname, $s, $e) = @_; | ||||||
2826 | $self->_link_on($s, $e); | ||||||
2827 | }, | ||||||
2828 | ToggleOff => sub { | ||||||
2829 | my $self = shift; | ||||||
2830 | my ($bname, $s, $e) = @_; | ||||||
2831 | $self->_link_off($s, $e); | ||||||
2832 | }}, | ||||||
2833 | |||||||
2834 | # 'left' - A paragraph tag, sets left justification. This is on | ||||||
2835 | # by default, and belongs to a group including 'right' and | ||||||
2836 | # 'center' - turning left on turns right and center off. | ||||||
2837 | # Turning it off turns it back on if right and center are | ||||||
2838 | # off. Used by the Left button (a toggle) | ||||||
2839 | left => {Class => 'paragraph', | ||||||
2840 | Look => {justification => 'left'}, | ||||||
2841 | Group => [qw(right center fill)], | ||||||
2842 | Default => 'left'}, | ||||||
2843 | |||||||
2844 | # 'right' - A paragraph tag, sets right justification. This | ||||||
2845 | # belongs to a group including 'left' and | ||||||
2846 | # 'center' - turning right on turns left and center off. | ||||||
2847 | # Turning it off turns left on. Used by the Right button | ||||||
2848 | # (a toggle) | ||||||
2849 | right => {Class => 'paragraph', | ||||||
2850 | Look => {justification => 'right'}, | ||||||
2851 | Group => [qw(left center fill)], | ||||||
2852 | Default => 'left'}, | ||||||
2853 | |||||||
2854 | # 'center' - A paragraph tag, sets centre justification. This | ||||||
2855 | # belongs to a group including 'left' and 'right' - | ||||||
2856 | # turning center on turns left and right off. | ||||||
2857 | # Turning it off turns left on. Used by the Center button | ||||||
2858 | # (a toggle) | ||||||
2859 | center => {Class => 'paragraph', | ||||||
2860 | Look => {justification => 'center'}, | ||||||
2861 | Group => [qw(left right fill)], | ||||||
2862 | Default => 'left'}, | ||||||
2863 | |||||||
2864 | fill => {Class => 'paragraph', | ||||||
2865 | Look => {justification => 'fill'}, | ||||||
2866 | Group => [qw(left right center)], | ||||||
2867 | Default => 'left'}, | ||||||
2868 | |||||||
2869 | indent => {Class => 'paragraph', | ||||||
2870 | ArgumentCount => 1}, | ||||||
2871 | |||||||
2872 | indentup => | ||||||
2873 | {Class => 'paragraph', | ||||||
2874 | Multi => 1, | ||||||
2875 | Activate => sub { | ||||||
2876 | my $self = shift; | ||||||
2877 | my ($bname, $s, $e) = @_; | ||||||
2878 | $self->_indent_up($s, $e); | ||||||
2879 | }}, | ||||||
2880 | |||||||
2881 | indentdown => | ||||||
2882 | {Class => 'paragraph', | ||||||
2883 | Multi => 1, | ||||||
2884 | Activate => sub { | ||||||
2885 | my $self = shift; | ||||||
2886 | my ($bname, $s, $e) = @_; | ||||||
2887 | $self->_indent_down($s, $e); | ||||||
2888 | }}, | ||||||
2889 | |||||||
2890 | # 'h1' to 'h5' - headings. Each is a member of the heading drop | ||||||
2891 | # down menu. | ||||||
2892 | h1 => {Class => 'paragraph', | ||||||
2893 | Look => {weight => PANGO_WEIGHT_BOLD, | ||||||
2894 | scale => 1.15 * 4}}, | ||||||
2895 | h2 => {Class => 'paragraph', | ||||||
2896 | Look => {weight => PANGO_WEIGHT_BOLD, | ||||||
2897 | scale => 1.15 * 3}}, | ||||||
2898 | h3 => {Class => 'paragraph', | ||||||
2899 | Look => {weight => PANGO_WEIGHT_BOLD, | ||||||
2900 | scale => 1.15 * 2}}, | ||||||
2901 | h4 => {Class => 'paragraph', | ||||||
2902 | Look => {weight => PANGO_WEIGHT_BOLD, | ||||||
2903 | scale => 1.15}}, | ||||||
2904 | h5 => {Class => 'paragraph', | ||||||
2905 | Look => {weight => PANGO_WEIGHT_BOLD, | ||||||
2906 | scale => 1.15, | ||||||
2907 | style => 'italic'}}, | ||||||
2908 | |||||||
2909 | size => {Class => 'font', | ||||||
2910 | Multi => 1, | ||||||
2911 | ArgumentCount => 1}, | ||||||
2912 | sizeup => {Class => 'font', | ||||||
2913 | Activate => sub { | ||||||
2914 | my $self = shift; | ||||||
2915 | $self->_increase_size; | ||||||
2916 | }}, | ||||||
2917 | sizedown => {Class => 'font', | ||||||
2918 | Activate => sub { | ||||||
2919 | my $self = shift; | ||||||
2920 | $self->_decrease_size; | ||||||
2921 | }}, | ||||||
2922 | |||||||
2923 | font => {Class => 'font', | ||||||
2924 | Multi => 1, | ||||||
2925 | ArgumentCount => 1}, | ||||||
2926 | |||||||
2927 | undo => {Class => 'undo', | ||||||
2928 | Activate => sub { | ||||||
2929 | my $self = shift; | ||||||
2930 | $self->undo; | ||||||
2931 | }}, | ||||||
2932 | |||||||
2933 | redo => {Class => 'undo', | ||||||
2934 | Activate => sub { | ||||||
2935 | my $self = shift; | ||||||
2936 | $self->redo; | ||||||
2937 | }}, | ||||||
2938 | # 'pre' - 'codifies' the included text, but other tags are honoured | ||||||
2939 | pre => {Class => 'font', | ||||||
2940 | Look => {family => 'Courier'}}, | ||||||
2941 | |||||||
2942 | # 'asis' - leaves the text exactly as is when exported | ||||||
2943 | asis => {Class => 'font', | ||||||
2944 | Look => {'background-full-height' => 1, | ||||||
2945 | background => 'blue', | ||||||
2946 | foreground => 'yellow'}}); | ||||||
2947 | } | ||||||
2948 | |||||||
2949 | # Buttons | ||||||
2950 | # Defines buttons that appear in the toolbar. The keys are button names (they | ||||||
2951 | # will be stored under the Buttons->NAME key of the WYSIWYG), values are | ||||||
2952 | # hashrefs with the following key/value pairs: | ||||||
2953 | # Type: what type of button to create. Valid options are 'button' (standard | ||||||
2954 | # clickable button), 'toggle' (toggle button), 'menu' (formatted menu | ||||||
2955 | # item), 'size' (numeric menu item) and 'font' (specialised menu) | ||||||
2956 | # Tag: a tag in the %TAGS hash that this button applies in some way. Note | ||||||
2957 | # that this might not be a direct mapping - it can just point to a tag | ||||||
2958 | # that has the right type of information, but isn't used directly. | ||||||
2959 | # TipText: a string, used to set tooltip text for the button. | ||||||
2960 | # Image: a stock image name to display on the button (for button and toggle | ||||||
2961 | # types only) | ||||||
2962 | # Label: a string to display on the buttons. CURRENTLY WILL REPLACE ANY | ||||||
2963 | # IMAGE GIVEN! | ||||||
2964 | # On: boolean, whether the toggle should be active once created | ||||||
2965 | # Tags: for menu types, this defines the menu items. Each element should be | ||||||
2966 | # an arrayref, the first element of which should be a tag name (used | ||||||
2967 | # to describe what to do when the menu item is chosen, and what the | ||||||
2968 | # menu item should look like), and the second element should be the | ||||||
2969 | # display text. If the tag name doesn't exist, it will be assumed that | ||||||
2970 | # that menu item is for the 'default' look, and no style will be | ||||||
2971 | # applied | ||||||
2972 | # Default: For menu types, this defines which item in the menu is the default | ||||||
2973 | # Width: for font types, this defines how wide (in characters) the menu | ||||||
2974 | # button should be. The menu button will show '...' at the end of | ||||||
2975 | # too-long items (the menu itself will still show them full width). | ||||||
2976 | BEGIN { | ||||||
2977 | %BUTTONS = (Clear => {Type => 'button', | ||||||
2978 | Tag => 'clear', | ||||||
2979 | Image => 'gtk-clear', | ||||||
2980 | TipText => 'Clear Formatting'}, | ||||||
2981 | Bold => {Tag => 'bold', | ||||||
2982 | Image => 'gtk-bold', | ||||||
2983 | Type => 'toggle', | ||||||
2984 | TipText => 'Bold'}, | ||||||
2985 | Italic => {Tag => 'italic', | ||||||
2986 | Image => 'gtk-italic', | ||||||
2987 | Type => 'toggle', | ||||||
2988 | TipText => 'Italic'}, | ||||||
2989 | Underline => {Tag => 'underline', | ||||||
2990 | Image => 'gtk-underline', | ||||||
2991 | Type => 'toggle', | ||||||
2992 | TipText => 'Underline'}, | ||||||
2993 | Strike => {Tag => 'strikethrough', | ||||||
2994 | Image => 'gtk-strikethrough', | ||||||
2995 | Type => 'toggle', | ||||||
2996 | TipText => 'Strikethrough'}, | ||||||
2997 | Link => {Tag => 'link', | ||||||
2998 | Image => 'gtk-network', | ||||||
2999 | Type => 'toggle', | ||||||
3000 | TipText => 'Add/Remove Link'}, | ||||||
3001 | Left => {Tag => 'left', | ||||||
3002 | Image => 'gtk-justify-left', | ||||||
3003 | Type => 'toggle', | ||||||
3004 | On => 1, | ||||||
3005 | TipText => 'Left Justify'}, | ||||||
3006 | Center => {Tag => 'center', | ||||||
3007 | Image => 'gtk-justify-center', | ||||||
3008 | Type => 'toggle', | ||||||
3009 | TipText => 'Center Justify'}, | ||||||
3010 | Right => {Tag => 'right', | ||||||
3011 | Image => 'gtk-justify-right', | ||||||
3012 | Type => 'toggle', | ||||||
3013 | TipText => 'Right Justify'}, | ||||||
3014 | Fill => {Tag => 'fill', | ||||||
3015 | Image => 'gtk-justify-fill', | ||||||
3016 | Type => 'toggle', | ||||||
3017 | TipText => 'Fill Justify'}, | ||||||
3018 | IndentUp => {Tag => 'indentup', | ||||||
3019 | Image => 'gtk-indent', | ||||||
3020 | Type => 'button', | ||||||
3021 | TipText => 'Increase Indent'}, | ||||||
3022 | IndentDown => {Tag => 'indentdown', | ||||||
3023 | Image => 'gtk-unindent', | ||||||
3024 | Type => 'button', | ||||||
3025 | TipText => 'Decrease Indent'}, | ||||||
3026 | Pre => {Tag => 'pre', | ||||||
3027 | Label => ' P ', | ||||||
3028 | Type => 'toggle', | ||||||
3029 | TipText => 'Keep Whitespace As Is'}, | ||||||
3030 | AsIs => {Tag => 'asis', | ||||||
3031 | Image => 'gtk-execute', | ||||||
3032 | Type => 'toggle', | ||||||
3033 | TipText => 'Code Mode'}, | ||||||
3034 | Heading => {Type => 'menu', | ||||||
3035 | Default => 'Normal', | ||||||
3036 | Tag => 'h1', # Typical tag | ||||||
3037 | Tags => [[h1 => 'Heading 1'], | ||||||
3038 | [h2 => 'Heading 2'], | ||||||
3039 | [h3 => 'Heading 3'], | ||||||
3040 | [h4 => 'Heading 4'], | ||||||
3041 | [h5 => 'Heading 5'], | ||||||
3042 | [h0 => 'Normal']]}, | ||||||
3043 | Size => {Type => 'size', | ||||||
3044 | Default => undef, | ||||||
3045 | Tag => 'size'}, | ||||||
3046 | SizeUp => {Type => 'button', | ||||||
3047 | Image => 'gtk-zoom-in', | ||||||
3048 | Tag => 'sizeup', | ||||||
3049 | TipText => 'Increase Font Size'}, | ||||||
3050 | SizeDown => {Type => 'button', | ||||||
3051 | Image => 'gtk-zoom-out', | ||||||
3052 | Tag => 'sizedown', | ||||||
3053 | TipText => 'Decrease Font Size'}, | ||||||
3054 | Font => {Type => 'font', | ||||||
3055 | Width => 20, | ||||||
3056 | Tag => 'font', ## FOR DISABLING! | ||||||
3057 | Default => undef, | ||||||
3058 | Tags => undef}, | ||||||
3059 | Sub => {Type => 'toggle', | ||||||
3060 | Image => 'gtk-go-down', | ||||||
3061 | Tag => 'subscript', | ||||||
3062 | TipText => 'Subscript'}, | ||||||
3063 | Super => {Type => 'toggle', | ||||||
3064 | Image => 'gtk-go-up', | ||||||
3065 | Tag => 'superscript', | ||||||
3066 | TipText => 'Superscript'}, | ||||||
3067 | # Case => {Type => 'button', | ||||||
3068 | # Image => 'gtk-cancel'}, | ||||||
3069 | # Colour => {Type => 'button', | ||||||
3070 | # Image => 'gtk-select-color'}, | ||||||
3071 | Undo => {Type => 'button', | ||||||
3072 | Tag => 'undo', | ||||||
3073 | Image => 'gtk-undo', | ||||||
3074 | TipText => 'Undo'}, | ||||||
3075 | Redo => {Type => 'button', | ||||||
3076 | Tag => 'redo', | ||||||
3077 | Image => 'gtk-redo', | ||||||
3078 | TipText => 'Redo'}); | ||||||
3079 | |||||||
3080 | } | ||||||
3081 | |||||||
3082 | BEGIN { | ||||||
3083 | package Gtk2::Ex::WYSIWYG::FormatMenu; | ||||||
3084 | |||||||
3085 | use strict; | ||||||
3086 | use Gtk2; | ||||||
3087 | use Gtk2::Pango; | ||||||
3088 | use Glib::Object::Subclass | ||||||
3089 | Gtk2::Button::, | ||||||
3090 | signals => {format_selected => {param_types => ['Glib::String', | ||||||
3091 | 'Glib::Scalar']}}; | ||||||
3092 | |||||||
3093 | sub INIT_INSTANCE { | ||||||
3094 | my $self = shift; | ||||||
3095 | my $hbox = Gtk2::HBox->new(0, 0); | ||||||
3096 | $self->{Label} = Gtk2::Label->new(); | ||||||
3097 | $self->{Options} = []; | ||||||
3098 | $self->{Default} = undef; | ||||||
3099 | $self->{Label}->set_alignment(0, 0.5); | ||||||
3100 | my $bar = Gtk2::VSeparator->new; | ||||||
3101 | my $arrow = Gtk2::Arrow->new('down', 'none'); | ||||||
3102 | $hbox->pack_start($self->{Label}, 1, 1, 0); | ||||||
3103 | $hbox->pack_start($bar, 0, 0, 2); | ||||||
3104 | $hbox->pack_start($arrow, 0, 0, 0); | ||||||
3105 | $hbox->show_all; | ||||||
3106 | $self->add($hbox); | ||||||
3107 | $self->signal_connect(clicked => sub {$self->_show_menu(@_)}); | ||||||
3108 | } | ||||||
3109 | |||||||
3110 | sub set_inconsistant { | ||||||
3111 | my $self = shift; | ||||||
3112 | $self->{Label}->set_text(''); | ||||||
3113 | } | ||||||
3114 | |||||||
3115 | sub get_inconsistant { | ||||||
3116 | my $self = shift; | ||||||
3117 | return $self->{Label}->get_text =~ /^\s*\z/; | ||||||
3118 | } | ||||||
3119 | |||||||
3120 | sub set_text { | ||||||
3121 | my $self = shift; | ||||||
3122 | my ($txt) = @_; | ||||||
3123 | $self->{Label}->set_text($txt); | ||||||
3124 | $self->{TT}->set_tip($self, $txt) if defined $self->{TT}; | ||||||
3125 | return 1; | ||||||
3126 | } | ||||||
3127 | |||||||
3128 | sub get_text { | ||||||
3129 | my $self = shift; | ||||||
3130 | $self->{Label}->get_text; | ||||||
3131 | } | ||||||
3132 | |||||||
3133 | sub set_default { | ||||||
3134 | my $self = shift; | ||||||
3135 | my ($default) = @_; | ||||||
3136 | if (not defined($default)) { | ||||||
3137 | $self->{Default} = undef; | ||||||
3138 | return 1; | ||||||
3139 | } | ||||||
3140 | for my $opt (@{$self->{Options}}) { | ||||||
3141 | next if $opt->[0] ne $default; | ||||||
3142 | $self->{Default} = $default; | ||||||
3143 | return 1; | ||||||
3144 | } | ||||||
3145 | die "Default string '$default' does not match any available options"; | ||||||
3146 | } | ||||||
3147 | |||||||
3148 | sub get_default { | ||||||
3149 | my $self = shift; | ||||||
3150 | return $self->{Default}; | ||||||
3151 | } | ||||||
3152 | |||||||
3153 | sub set_options { | ||||||
3154 | my $self = shift; | ||||||
3155 | my @opts = @_; | ||||||
3156 | for my $opt (@opts) { | ||||||
3157 | # Need DISPLAY, DAT, and STYLE - STR, ANY, HASHREF | ||||||
3158 | die "Option style must be a hashref or undef" | ||||||
3159 | if defined($opt->[2]) and ref($opt->[2]) ne 'HASH'; | ||||||
3160 | } | ||||||
3161 | $self->{Options} = []; | ||||||
3162 | for my $opt (@opts) { | ||||||
3163 | push @{$self->{Options}}, ["$opt->[0]", $opt->[1], $opt->[2]]; | ||||||
3164 | } | ||||||
3165 | return 1; | ||||||
3166 | } | ||||||
3167 | |||||||
3168 | sub get_options { | ||||||
3169 | my $self = shift; | ||||||
3170 | return map({[$_->[0], $_->[1], ref($_->[2]) ? {%{$_->[2]}} : undef]} | ||||||
3171 | @{$self->{Options}}); | ||||||
3172 | } | ||||||
3173 | |||||||
3174 | sub get_tool_tip { | ||||||
3175 | my $self = shift; | ||||||
3176 | return $self->{TT}; | ||||||
3177 | } | ||||||
3178 | |||||||
3179 | sub set_tool_tip { | ||||||
3180 | my $self = shift; | ||||||
3181 | my ($TT) = @_; | ||||||
3182 | $self->{TT} = $TT; | ||||||
3183 | $self->{TT}->set_tip($self, $self->{Label}->get_text) | ||||||
3184 | if defined $self->{TT}; | ||||||
3185 | } | ||||||
3186 | |||||||
3187 | sub set_width_chars { | ||||||
3188 | my $self = shift; | ||||||
3189 | $self->{Label}->set_width_chars(@_); | ||||||
3190 | } | ||||||
3191 | |||||||
3192 | sub get_width_chars { | ||||||
3193 | my $self = shift; | ||||||
3194 | $self->{Label}->get_width_chars(@_); | ||||||
3195 | } | ||||||
3196 | |||||||
3197 | sub set_ellipsize { | ||||||
3198 | my $self = shift; | ||||||
3199 | $self->{Label}->set_ellipsize(@_); | ||||||
3200 | } | ||||||
3201 | |||||||
3202 | sub get_ellipsize { | ||||||
3203 | my $self = shift; | ||||||
3204 | $self->{Label}->get_ellipsize(@_); | ||||||
3205 | } | ||||||
3206 | |||||||
3207 | sub _show_menu { | ||||||
3208 | my $self = shift; | ||||||
3209 | return 0 if not scalar(@{$self->{Options}}); | ||||||
3210 | my $menu = Gtk2::Menu->new; | ||||||
3211 | my $match = $self->{Label}->get_text; | ||||||
3212 | my $sel = undef; | ||||||
3213 | my $i = 0; | ||||||
3214 | for my $opt (@{$self->{Options}}) { | ||||||
3215 | my ($label, $dat, $style) = @$opt; | ||||||
3216 | if ($label eq $match) { | ||||||
3217 | $sel = $i; | ||||||
3218 | } elsif (not defined $sel and defined $self->{Default} and | ||||||
3219 | $label eq $self->{Default}) { | ||||||
3220 | $sel = $i; | ||||||
3221 | } | ||||||
3222 | ++$i; | ||||||
3223 | my $item = Gtk2::MenuItem->new_with_label(''); | ||||||
3224 | if (defined($style)) { | ||||||
3225 | my @slist; | ||||||
3226 | for my $attr (keys %$style) { | ||||||
3227 | if ($attr eq 'scale') { | ||||||
3228 | my $s = $self->get_pango_context->get_font_description-> | ||||||
3229 | get_size; | ||||||
3230 | $s = int($s * $style->{$attr}); | ||||||
3231 | push @slist, "size=\"$s\""; | ||||||
3232 | } elsif ($attr eq 'family') { | ||||||
3233 | push @slist, "font_family=\"$style->{$attr}\""; | ||||||
3234 | } else { | ||||||
3235 | push @slist, "$attr=\"$style->{$attr}\""; | ||||||
3236 | } | ||||||
3237 | } | ||||||
3238 | my $lab = $item->get_child; | ||||||
3239 | if (scalar(@slist)) { | ||||||
3240 | my $vis = $label; | ||||||
3241 | $vis =~ s/</g; | ||||||
3242 | $lab->set_markup("$vis"); | ||||||
3243 | } else { | ||||||
3244 | $lab->set_text($label); | ||||||
3245 | } | ||||||
3246 | } else { | ||||||
3247 | $item->get_child->set_text($label); | ||||||
3248 | } | ||||||
3249 | $item->signal_connect(activate => sub { | ||||||
3250 | $self->_item_selected($label, $dat); | ||||||
3251 | }); | ||||||
3252 | $item->show; | ||||||
3253 | $menu->append($item); | ||||||
3254 | } | ||||||
3255 | $sel = 0 if not defined $sel; | ||||||
3256 | $menu->set_active($sel); | ||||||
3257 | # Popup the menu | ||||||
3258 | $menu->popup(undef, undef, undef, undef, $self, undef); | ||||||
3259 | $menu->popup(undef, undef, '_menu_pos', $self, $self, undef); | ||||||
3260 | my ($mx, $my) = $menu->get_size_request; | ||||||
3261 | my ($bx, $by) = $self->get_size_request; | ||||||
3262 | $menu->set_size_request($bx, -1) if $mx < $bx; | ||||||
3263 | my $active = $menu->get_active; | ||||||
3264 | ($active) = $menu->get_children if not defined $active; | ||||||
3265 | $menu->select_item($active); | ||||||
3266 | return 0; | ||||||
3267 | } | ||||||
3268 | |||||||
3269 | # !!! _menu_pos assumes that the menu _HAS ALREADY BEEN POPPED UP!_ | ||||||
3270 | # This is so allocation details are set already. | ||||||
3271 | sub _menu_pos { | ||||||
3272 | my ($menu, $evx, $evy, $self) = @_; | ||||||
3273 | my ($px, $py) = $self->get_pointer; | ||||||
3274 | my ($x, $y, $w, $h) = $self->allocation->values; | ||||||
3275 | my ($rx, $ry) = $self->window->get_origin; | ||||||
3276 | my $active = $menu->get_active; | ||||||
3277 | ($active) = $menu->get_children if not defined $active; | ||||||
3278 | my ($ix, $iy, $iw, $ih) = $active->allocation->values; | ||||||
3279 | return ($rx + $x, $evy - $iy - ($ih / 2)); | ||||||
3280 | } | ||||||
3281 | |||||||
3282 | sub _item_selected { | ||||||
3283 | my $self = shift; | ||||||
3284 | my ($disp, $dat) = @_; | ||||||
3285 | $self->{Label}->set_text($disp); | ||||||
3286 | $self->{TT}->set_tip($self, $disp) if defined $self->{TT}; | ||||||
3287 | $self->signal_emit(format_selected => $disp, $dat); | ||||||
3288 | return 0; | ||||||
3289 | } | ||||||
3290 | } | ||||||
3291 | |||||||
3292 | BEGIN { | ||||||
3293 | package Gtk2::Ex::WYSIWYG::SizeMenu; | ||||||
3294 | |||||||
3295 | use strict; | ||||||
3296 | use Gtk2; | ||||||
3297 | use Gtk2::Pango; | ||||||
3298 | use Glib::Object::Subclass | ||||||
3299 | Gtk2::ComboBoxEntry::, | ||||||
3300 | signals => {size_selected => {param_types => ['Glib::UInt']}}; | ||||||
3301 | |||||||
3302 | my @DEFAULT_SIZES = qw(8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72); | ||||||
3303 | sub INIT_INSTANCE { | ||||||
3304 | my $self = shift; | ||||||
3305 | my $model = Gtk2::ListStore->new('Glib::String'); | ||||||
3306 | for my $val (@DEFAULT_SIZES) { | ||||||
3307 | $model->set($model->append, 0, $val); | ||||||
3308 | } | ||||||
3309 | $self->set_model($model); | ||||||
3310 | $self->set_text_column(0); | ||||||
3311 | my $ent = $self->get_child; # -> validation! | ||||||
3312 | $ent->set_max_length(4); # 1 to 1024pt | ||||||
3313 | $ent->set_width_chars(4); | ||||||
3314 | $self->signal_connect(changed => sub {$self->_changed(@_)}); | ||||||
3315 | } | ||||||
3316 | |||||||
3317 | sub set_inconsistant { | ||||||
3318 | my $self = shift; | ||||||
3319 | $self->get_child->set_text(''); | ||||||
3320 | } | ||||||
3321 | |||||||
3322 | sub get_inconsistant { | ||||||
3323 | my $self = shift; | ||||||
3324 | return $self->get_child->get_text =~ /^\s*\z/; | ||||||
3325 | } | ||||||
3326 | |||||||
3327 | sub set_value { | ||||||
3328 | my $self = shift; | ||||||
3329 | my ($val) = @_; | ||||||
3330 | die "Cannot set value to non-numeric" if $val =~ /\D/ or not length($val); | ||||||
3331 | die "Cannot set value to zero" if not $val; | ||||||
3332 | die "Maximum value is 1024" if $val > 1024; | ||||||
3333 | $self->get_child->set_text($val); | ||||||
3334 | $self->{OldValue} = $val; | ||||||
3335 | return 1; | ||||||
3336 | } | ||||||
3337 | |||||||
3338 | sub get_value { | ||||||
3339 | my $self = shift; | ||||||
3340 | my $res = $self->get_child->get_text; | ||||||
3341 | $res = 1 if not $res; | ||||||
3342 | return $res; | ||||||
3343 | } | ||||||
3344 | |||||||
3345 | sub up_value { | ||||||
3346 | my $self = shift; | ||||||
3347 | my $curr = $self->get_value; | ||||||
3348 | my $new = $self->next_value_up($curr); | ||||||
3349 | return if $new == $curr; | ||||||
3350 | $self->set_value($new); | ||||||
3351 | } | ||||||
3352 | |||||||
3353 | sub down_value { | ||||||
3354 | my $self = shift; | ||||||
3355 | my $curr = $self->get_value; | ||||||
3356 | my $new = $self->next_value_down($curr); | ||||||
3357 | return if $new == $curr; | ||||||
3358 | $self->set_value($new); | ||||||
3359 | } | ||||||
3360 | |||||||
3361 | sub next_value_up { | ||||||
3362 | my $self = shift; | ||||||
3363 | my ($from) = @_; | ||||||
3364 | return 1024 if $from >= 1024; | ||||||
3365 | return $from + 1 | ||||||
3366 | if $from < $DEFAULT_SIZES[0] or $from >= $DEFAULT_SIZES[-1]; | ||||||
3367 | for my $i (0..(scalar(@DEFAULT_SIZES) - 2)) { | ||||||
3368 | next if $DEFAULT_SIZES[$i] < $from; | ||||||
3369 | return $DEFAULT_SIZES[$i + 1] if $from == $DEFAULT_SIZES[$i]; | ||||||
3370 | last; | ||||||
3371 | } | ||||||
3372 | return $from + 1; | ||||||
3373 | } | ||||||
3374 | |||||||
3375 | sub next_value_down { | ||||||
3376 | my $self = shift; | ||||||
3377 | my ($from) = @_; | ||||||
3378 | return 1 if $from <= 1; | ||||||
3379 | return $from - 1 | ||||||
3380 | if $from <= $DEFAULT_SIZES[0] or $from > $DEFAULT_SIZES[-1]; | ||||||
3381 | for my $i (1..(scalar(@DEFAULT_SIZES) - 1)) { | ||||||
3382 | next if $DEFAULT_SIZES[$i] < $from; | ||||||
3383 | return $DEFAULT_SIZES[$i - 1] if $from == $DEFAULT_SIZES[$i]; | ||||||
3384 | last; | ||||||
3385 | } | ||||||
3386 | return $from - 1; | ||||||
3387 | } | ||||||
3388 | |||||||
3389 | sub _changed { | ||||||
3390 | my $self = shift; | ||||||
3391 | return 0 if $self->{STOP}; | ||||||
3392 | my $curr = $self->get_child->get_text; | ||||||
3393 | if ($curr =~ /\D/) { | ||||||
3394 | ++$self->{STOP}; | ||||||
3395 | $self->get_child->set_text($self->{OldValue}); | ||||||
3396 | --$self->{STOP}; | ||||||
3397 | return 0 | ||||||
3398 | } | ||||||
3399 | $self->{OldValue} = $curr; | ||||||
3400 | $self->signal_emit(size_selected => $self->get_child->get_text); | ||||||
3401 | return 1; | ||||||
3402 | } | ||||||
3403 | } | ||||||
3404 | |||||||
3405 | BEGIN { | ||||||
3406 | package Gtk2::Ex::WYSIWYG::HTML; | ||||||
3407 | |||||||
3408 | use strict; | ||||||
3409 | use XML::Quote; | ||||||
3410 | use constant CLEV_PARAGRAPH => 5; | ||||||
3411 | use constant CLEV_PRE => 4; | ||||||
3412 | use constant CLEV_SPAN => 3; | ||||||
3413 | use constant CLEV_SUPSUB => 2; | ||||||
3414 | use constant CLEV_LINK => 1; | ||||||
3415 | use constant CLEV_NONE => 0; | ||||||
3416 | |||||||
3417 | my (@TAGS, @FONTS); | ||||||
3418 | my ($TPOS, $HPOS, $TXT, $DEFAULT_SIZE) = (0, 0, '', 10); | ||||||
3419 | |||||||
3420 | sub init { | ||||||
3421 | @TAGS = (); | ||||||
3422 | $TPOS = 0; | ||||||
3423 | $HPOS = 0; | ||||||
3424 | $TXT = ''; | ||||||
3425 | } | ||||||
3426 | |||||||
3427 | sub set_fonts { | ||||||
3428 | my $class = shift; | ||||||
3429 | @FONTS = @_; | ||||||
3430 | } | ||||||
3431 | |||||||
3432 | sub set_default_size { | ||||||
3433 | my $class = shift; | ||||||
3434 | $DEFAULT_SIZE = $_[0]; | ||||||
3435 | } | ||||||
3436 | |||||||
3437 | sub _check_start { | ||||||
3438 | my $class = shift; | ||||||
3439 | my ($pok, $preok, $spanok, $subok, $aok) = @_; | ||||||
3440 | for my $i (reverse(0..(scalar(@TAGS) - 1))) { | ||||||
3441 | for my $chk ([$pok, [qw(P H1 H2 H3 H4 H5)]], | ||||||
3442 | [$preok, ['PRE']], | ||||||
3443 | [$spanok, ['SPAN']], | ||||||
3444 | [$subok, [qw(SUB SUP)]], | ||||||
3445 | [$aok, ['A']]) { | ||||||
3446 | my ($ok, $types) = @$chk; | ||||||
3447 | next if $ok; | ||||||
3448 | for my $type (@$types) { | ||||||
3449 | return 0 if $TAGS[$i]{Type} eq $type and not defined $TAGS[$i]{End}; | ||||||
3450 | } | ||||||
3451 | } | ||||||
3452 | } | ||||||
3453 | return 1; | ||||||
3454 | } | ||||||
3455 | |||||||
3456 | sub _check_end { | ||||||
3457 | my $class = shift; | ||||||
3458 | my ($type, $seena, $seensub, $seenspan, $seenpre) = @_; | ||||||
3459 | my $open; | ||||||
3460 | # Paragraph tags could enclose arbitrary numbers of CLOSED font tags | ||||||
3461 | my $para = grep({$_ eq $type} qw(P H1 H2 H3 H4 H5)); | ||||||
3462 | for my $ti (reverse(0..(scalar(@TAGS) - 1))) { | ||||||
3463 | my $ct = $TAGS[$ti]; | ||||||
3464 | if ($ct->{Type} eq $type) { | ||||||
3465 | $open = $ct if not defined $ct->{End}; | ||||||
3466 | last; | ||||||
3467 | } elsif ($ct->{Type} =~ /^H\d\z/ or $ct->{Type} eq 'P') { | ||||||
3468 | last; | ||||||
3469 | } elsif ($ct->{Type} eq 'PRE') { | ||||||
3470 | last if (not $para and $seenpre) or not defined($ct->{End}); | ||||||
3471 | $seenpre = 1; | ||||||
3472 | } elsif ($ct->{Type} eq 'SPAN') { | ||||||
3473 | last if ((not $para and ($seenpre or $seenspan)) or | ||||||
3474 | not defined($ct->{End})); | ||||||
3475 | $seenspan = 1; | ||||||
3476 | } elsif ($ct->{Type} eq 'SUB' or $ct->{Type} eq 'SUP') { | ||||||
3477 | last if ((not $para and ($seenpre or $seenspan or $seensub)) or | ||||||
3478 | not defined($ct->{End})); | ||||||
3479 | $seensub = 1; | ||||||
3480 | } elsif ($ct->{Type} eq 'A') { | ||||||
3481 | last if ((not $para and ($seenpre or $seenspan or | ||||||
3482 | $seensub or $seena)) or | ||||||
3483 | not defined($ct->{End})); | ||||||
3484 | $seena = 1; | ||||||
3485 | } else { | ||||||
3486 | last; | ||||||
3487 | } | ||||||
3488 | } | ||||||
3489 | return $open; | ||||||
3490 | } | ||||||
3491 | |||||||
3492 | sub _tag_asis { | ||||||
3493 | my $class = shift; | ||||||
3494 | my ($tag) = @_; | ||||||
3495 | $TXT .= $tag; | ||||||
3496 | push @TAGS, {Type => 'ASIS', | ||||||
3497 | Start => $TPOS, | ||||||
3498 | End => $TPOS + length($tag), | ||||||
3499 | Tags => {asis => undef}}; | ||||||
3500 | $TPOS += length($tag); | ||||||
3501 | $HPOS += length($tag); | ||||||
3502 | } | ||||||
3503 | |||||||
3504 | sub _handle_open_tag { | ||||||
3505 | my $class = shift; | ||||||
3506 | my ($tag, $type, $style, $flags, $look) = @_; | ||||||
3507 | if (not $class->_check_start(@$flags)) { | ||||||
3508 | $class->_tag_asis($tag); | ||||||
3509 | return; | ||||||
3510 | } | ||||||
3511 | my $stags = (defined($style) ? $class->_parse_style($style) : {});### | ||||||
3512 | if (not defined($stags)) { | ||||||
3513 | $class->_tag_asis($tag); | ||||||
3514 | return; | ||||||
3515 | } | ||||||
3516 | for my $k (keys %$look) { | ||||||
3517 | $stags->{$k} = $look->{$k}; | ||||||
3518 | } | ||||||
3519 | push @TAGS, {Type => $type, | ||||||
3520 | Start => $TPOS, | ||||||
3521 | Tags => $stags}; | ||||||
3522 | $HPOS += length($tag); | ||||||
3523 | } | ||||||
3524 | |||||||
3525 | sub _handle_close_tag { | ||||||
3526 | my $class = shift; | ||||||
3527 | my ($tag, $type, $flags, $nl) = @_; | ||||||
3528 | my $open = $class->_check_end($type, @$flags); | ||||||
3529 | if (defined($open)) { | ||||||
3530 | $open->{End} = $TPOS; | ||||||
3531 | $HPOS += length($tag); | ||||||
3532 | if ($nl) { | ||||||
3533 | $TXT .= "\n"; | ||||||
3534 | ++$TPOS; | ||||||
3535 | } | ||||||
3536 | return; | ||||||
3537 | } | ||||||
3538 | $class->_tag_asis($tag); | ||||||
3539 | } | ||||||
3540 | |||||||
3541 | sub _parse_style { | ||||||
3542 | my $class = shift; | ||||||
3543 | my ($style) = @_; | ||||||
3544 | my %tags; | ||||||
3545 | for my $part (grep {$_ !~ /^\s*\z/} split(/\s*;\s*/, $style)) { | ||||||
3546 | $part =~ s/(?:^\s+)|(\s+\z)//; | ||||||
3547 | my ($key, $val) = split(/\s*:\s*/, $part, 2); | ||||||
3548 | $key = lc($key); | ||||||
3549 | if ($key eq 'font-weight') { | ||||||
3550 | return undef if lc($val) ne 'bold'; | ||||||
3551 | $tags{bold} = undef; | ||||||
3552 | } elsif ($key eq 'font-style') { | ||||||
3553 | return undef if lc($val) ne 'italic'; | ||||||
3554 | $tags{italic} = undef; | ||||||
3555 | } elsif ($key eq 'font-size') { | ||||||
3556 | return undef if $val !~ /^(\d+(?:\.\d+)?)[Ee][Mm]\z/; | ||||||
3557 | $tags{size} = [int($1 * 16)]; | ||||||
3558 | } elsif ($key eq 'font-family') { | ||||||
3559 | return undef if not grep {$_ eq $val} @FONTS; | ||||||
3560 | $tags{font} = [$val]; | ||||||
3561 | } elsif ($key eq 'text-decoration') { | ||||||
3562 | for my $sval (grep {$_ !~ /^\s*\z/} split(/\s+/, lc($val))) { | ||||||
3563 | if ($sval eq 'underline') { | ||||||
3564 | $tags{underline} = undef; | ||||||
3565 | } elsif ($sval eq 'line-through') { | ||||||
3566 | $tags{strikethrough} = undef; | ||||||
3567 | } else { | ||||||
3568 | return undef; | ||||||
3569 | } | ||||||
3570 | } | ||||||
3571 | } elsif ($key eq 'text-align') { | ||||||
3572 | $val = lc($val); | ||||||
3573 | return undef if not grep {$_ eq $val} qw(left center right justify); | ||||||
3574 | $val = 'fill' if $val eq 'justify'; | ||||||
3575 | $tags{$val} = undef; | ||||||
3576 | } elsif ($key eq 'margin-left' or $key eq 'margin-right') { | ||||||
3577 | return undef if lc($val) !~ /^(\d+)px\z/; | ||||||
3578 | my $cnt = $1; | ||||||
3579 | $cnt /= 32; | ||||||
3580 | return undef if int($cnt) != $cnt; | ||||||
3581 | $tags{indent} = [$cnt]; | ||||||
3582 | } else { | ||||||
3583 | return undef; | ||||||
3584 | } | ||||||
3585 | } | ||||||
3586 | return \%tags; | ||||||
3587 | } | ||||||
3588 | |||||||
3589 | sub _html_style { | ||||||
3590 | my $class = shift; | ||||||
3591 | my ($style) = @_; | ||||||
3592 | my @sstyle; | ||||||
3593 | push @sstyle, 'font-weight:bold' if exists $style->{bold}; | ||||||
3594 | push @sstyle, 'font-style:italic' if exists $style->{italic}; | ||||||
3595 | push @sstyle, sprintf('font-size:%.3fem', | ||||||
3596 | ($style->{size}[0] / 16))#$DEFAULT_SIZE)) | ||||||
3597 | if exists $style->{size}; | ||||||
3598 | push @sstyle, "font-family:$style->{font}[0]" | ||||||
3599 | if exists $style->{font}; | ||||||
3600 | my @deco; | ||||||
3601 | push @deco, 'underline' if exists $style->{underline}; | ||||||
3602 | push @deco, 'line-through' if exists $style->{strikethrough}; | ||||||
3603 | push @sstyle, 'text-decoration:' . join(' ', @deco) if scalar(@deco); | ||||||
3604 | return @sstyle; | ||||||
3605 | } | ||||||
3606 | |||||||
3607 | sub _get_html_tag_changelevel { | ||||||
3608 | my $self = shift; | ||||||
3609 | my ($new, $old) = @_; | ||||||
3610 | return CLEV_PARAGRAPH | ||||||
3611 | if (not defined($old->{paragraph_type}) or | ||||||
3612 | $old->{paragraph_type} ne $new->{paragraph_type} or | ||||||
3613 | $old->{align} ne $new->{align} or | ||||||
3614 | $old->{indent} ne $new->{indent}); | ||||||
3615 | return CLEV_PRE if exists($old->{pre}) != exists($new->{pre}); | ||||||
3616 | for my $stag (qw(bold italic underline strikethrough)) { | ||||||
3617 | return CLEV_SPAN if exists($old->{$stag}) != exists($new->{$stag}); | ||||||
3618 | } | ||||||
3619 | for my $stag (qw(font size)) { | ||||||
3620 | return CLEV_SPAN | ||||||
3621 | if (exists($old->{$stag}) != exists($new->{$stag}) or | ||||||
3622 | (exists $old->{$stag} and $old->{$stag} ne $new->{$stag})); | ||||||
3623 | } | ||||||
3624 | return CLEV_SUPSUB | ||||||
3625 | if (exists($old->{superscript}) != exists($new->{superscript}) or | ||||||
3626 | exists($old->{subscript}) != exists($new->{subscript})); | ||||||
3627 | return CLEV_LINK | ||||||
3628 | if (exists($old->{link}) != exists($new->{link}) or | ||||||
3629 | (exists $old->{link} and $new->{link} ne $old->{link})); | ||||||
3630 | return CLEV_NONE; | ||||||
3631 | } | ||||||
3632 | |||||||
3633 | sub _get_html_tag_state { | ||||||
3634 | my $class = shift; | ||||||
3635 | my ($tag) = @_; | ||||||
3636 | my $def = {paragraph_type => 'p', | ||||||
3637 | align => undef, | ||||||
3638 | indent => undef}; | ||||||
3639 | for my $tname (keys %{$tag->{Tags}}) { | ||||||
3640 | if ($tname =~ /^h[1-5]\z/) { | ||||||
3641 | $def->{paragraph_type} = $tname; | ||||||
3642 | } elsif ($tname eq 'indent') { | ||||||
3643 | $def->{indent} = [$tag->{Tags}{$tname}]; | ||||||
3644 | } elsif (grep {$_ eq $tname} qw(right center)) { | ||||||
3645 | $def->{align} = $tname; | ||||||
3646 | } elsif ($tname eq 'fill') { | ||||||
3647 | $def->{align} = 'justify'; | ||||||
3648 | } else { | ||||||
3649 | $def->{$tname} = $tag->{Tags}{$tname}; | ||||||
3650 | } | ||||||
3651 | } | ||||||
3652 | return $def; | ||||||
3653 | } | ||||||
3654 | |||||||
3655 | sub parse { | ||||||
3656 | my $class = shift; | ||||||
3657 | my ($html) = @_; | ||||||
3658 | $class->init; | ||||||
3659 | while ($HPOS < length($html)) { | ||||||
3660 | my $char = substr($html, $HPOS, 1); | ||||||
3661 | if ($char ne '<') { | ||||||
3662 | # Slurp up to next tag | ||||||
3663 | my $txt = $char; | ||||||
3664 | ++$HPOS; | ||||||
3665 | $char = undef; | ||||||
3666 | while (1) { | ||||||
3667 | last if $HPOS >= length($html); | ||||||
3668 | $char = substr($html, $HPOS, 1); | ||||||
3669 | last if $char eq '<'; | ||||||
3670 | $txt .= $char; | ||||||
3671 | ++$HPOS; | ||||||
3672 | } | ||||||
3673 | $txt = xml_dequote($txt); | ||||||
3674 | $TXT .= $txt; | ||||||
3675 | $TPOS += length($txt); | ||||||
3676 | next; | ||||||
3677 | } | ||||||
3678 | # New tag? | ||||||
3679 | my $tag = '<'; | ||||||
3680 | my $j = $HPOS + 1; | ||||||
3681 | while ($j < length($html)) { | ||||||
3682 | $char = substr($html, $j++, 1); | ||||||
3683 | $tag .= $char; | ||||||
3684 | last if $char eq '>'; | ||||||
3685 | } | ||||||
3686 | if (index($tag, '>') == -1) { | ||||||
3687 | $class->_tag_asis($tag); | ||||||
3688 | next; | ||||||
3689 | } | ||||||
3690 | my ($close, $type, $style, $nl, $look, $flags) = | ||||||
3691 | (0, undef, undef, 0, {}, []); | ||||||
3692 | if ($tag =~ /^ \z/) { |
||||||
3693 | $TXT .= "\n"; | ||||||
3694 | $TPOS += 1; | ||||||
3695 | $HPOS += length($tag); | ||||||
3696 | next; | ||||||
3697 | } elsif ($tag =~ /^\z/) { | ||||||
3698 | # Self contained - other tags don't matter | ||||||
3699 | # WS Tag | ||||||
3700 | my $jump = $HPOS + length($tag); | ||||||
3701 | my $ws = ''; | ||||||
3702 | # get as much whitespace as possible, then grab a | ||||||
3703 | my $close = undef; | ||||||
3704 | my $ok = 0; | ||||||
3705 | while ($jump < length($html)) { | ||||||
3706 | my $char = substr($html, $jump++, 1); | ||||||
3707 | if (defined($close)) { | ||||||
3708 | $close .= $char; | ||||||
3709 | if ($close eq '') { | ||||||
3710 | $ok = 1; | ||||||
3711 | last; | ||||||
3712 | } | ||||||
3713 | last if '' !~ /^\Q$close/; | ||||||
3714 | } elsif ($char eq '<') { | ||||||
3715 | $close = $char; | ||||||
3716 | } elsif ($char eq "\n" or $char !~ /^\s\z/) { | ||||||
3717 | last; | ||||||
3718 | } else { | ||||||
3719 | $ws .= $char; | ||||||
3720 | } | ||||||
3721 | } | ||||||
3722 | if (not $ok) { | ||||||
3723 | $class->_tag_asis($tag); | ||||||
3724 | } else { | ||||||
3725 | $TXT .= $ws; | ||||||
3726 | $TPOS += $ws; | ||||||
3727 | $HPOS += $jump; | ||||||
3728 | } | ||||||
3729 | next; | ||||||
3730 | } elsif ($tag =~ /^<(p|h1|h2|h3|h4|h5)(?:\s+style=\"([^\"]+)\")?>\z/i) { | ||||||
3731 | ($type, $style) = (uc($1), $2); | ||||||
3732 | $flags = [0, 0, 0, 0, 0]; | ||||||
3733 | $look->{$type} = undef if $type ne 'P'; | ||||||
3734 | } elsif ($tag =~ /^<\/(p|h1|h2|h3|h4|h5)>\z/) { | ||||||
3735 | ($close, $type, $flags, $nl) = (1, uc($1), [0, 0, 0, 0], 1); | ||||||
3736 | } elsif ($tag eq '') { |
||||||
3737 | ($type, $flags) = ('PRE', [1, 0, 0, 0, 0]); | ||||||
3738 | $look->{pre} = undef; | ||||||
3739 | } elsif ($tag eq '') { | ||||||
3740 | ($close, $type, $flags) = (1, 'PRE', [0, 0, 0, 1]); | ||||||
3741 | } elsif ($tag =~ /^\z/) { | ||||||
3742 | ($type, $style, $flags) = ('SPAN', $1, [1, 1, 0, 0, 0]); | ||||||
3743 | } elsif ($tag eq '') { | ||||||
3744 | ($close, $type, $flags) = (1, 'SPAN', [0, 0, 1, 1]); | ||||||
3745 | } elsif ($tag eq '' or $tag eq '') { | ||||||
3746 | $type = uc($tag); | ||||||
3747 | $type =~ s/[<>]//g; | ||||||
3748 | $look->{$type eq 'SUP' ? 'superscript' : 'subscript'} = undef; | ||||||
3749 | $flags = [1, 1, 1, 0, 0]; | ||||||
3750 | } elsif ($tag eq '' or $tag eq '') { | ||||||
3751 | $close = 1; | ||||||
3752 | $type = uc($tag); | ||||||
3753 | $type =~ s/[<>]//g; | ||||||
3754 | $flags = [0, 1, 1, 1]; | ||||||
3755 | } elsif ($tag =~ /^\z/) { | ||||||
3756 | # There should be no open a tags | ||||||
3757 | $look->{link} = $1; | ||||||
3758 | ($type, $flags) = ('A', [1, 1, 1, 1, 0]); | ||||||
3759 | } elsif ($tag eq '') { | ||||||
3760 | ($close, $type, $flags) = (1, 'A', [1, 1, 1, 1]); | ||||||
3761 | } else { | ||||||
3762 | $class->_tag_asis($tag); | ||||||
3763 | next; | ||||||
3764 | } | ||||||
3765 | if ($close) { | ||||||
3766 | $class->_handle_close_tag($tag, $type, $flags, $nl); | ||||||
3767 | } else { | ||||||
3768 | $class->_handle_open_tag($tag, $type, $style, $flags, $look); | ||||||
3769 | } | ||||||
3770 | } | ||||||
3771 | for my $i (0..(scalar(@TAGS) - 2)) { | ||||||
3772 | next if defined($TAGS[$i]{End}); | ||||||
3773 | $TAGS[$i]{End} = $TAGS[$i + 1]{Start}; | ||||||
3774 | } | ||||||
3775 | if (scalar(@TAGS)) { | ||||||
3776 | $TAGS[-1]{End} = $TPOS if not defined($TAGS[-1]{End}); | ||||||
3777 | @TAGS = grep {scalar(keys %{$_->{Tags}})} @TAGS; | ||||||
3778 | for my $tag (@TAGS) { | ||||||
3779 | delete($tag->{Type}); | ||||||
3780 | } | ||||||
3781 | } | ||||||
3782 | my ($txt, @tags) = ($TXT, @TAGS); | ||||||
3783 | $class->init; | ||||||
3784 | return ($txt, @tags); | ||||||
3785 | } | ||||||
3786 | |||||||
3787 | sub generate { | ||||||
3788 | my $class = shift; | ||||||
3789 | my ($buf, @tags) = @_; | ||||||
3790 | my $res = ''; | ||||||
3791 | if (not scalar(@tags)) { | ||||||
3792 | $res .= " "; |
||||||
3793 | $res .= xml_quote($buf->get_text($buf->get_bounds, 0)); | ||||||
3794 | $res .= "\n"; | ||||||
3795 | return $res; | ||||||
3796 | } | ||||||
3797 | my @openstack; | ||||||
3798 | my $currstyle = {paragraph_type => undef, | ||||||
3799 | indent => undef, | ||||||
3800 | align => undef}; | ||||||
3801 | if ($tags[0]{Start} != 0) { | ||||||
3802 | $res .= " "; |
||||||
3803 | push @openstack, {name => 'p', | ||||||
3804 | type => 'paragraph'}; | ||||||
3805 | $currstyle->{paragraph_type} = 'p'; | ||||||
3806 | } | ||||||
3807 | my $lastpos = 0; | ||||||
3808 | for my $tag (@tags) { | ||||||
3809 | # Previous text... | ||||||
3810 | if ($lastpos != $tag->{Start}) { | ||||||
3811 | # Turn off all non-paragraph tags! | ||||||
3812 | while (scalar(@openstack)) { | ||||||
3813 | last if $openstack[-1]{type} eq 'paragraph'; | ||||||
3814 | my $this = pop(@openstack); | ||||||
3815 | $res .= "$this->{name}>"; | ||||||
3816 | } | ||||||
3817 | $currstyle = {paragraph_type => $currstyle->{paragraph_type}, | ||||||
3818 | align => $currstyle->{align}, | ||||||
3819 | indent => $currstyle->{indent}}; | ||||||
3820 | # And if there's no paragraph tag here yet?! The only way that could | ||||||
3821 | # happen is if there were no paragraph tags, and the only way that | ||||||
3822 | # could happen if it's going to be an empty, plain
|
||||||
3823 | if (not scalar(@openstack)) { | ||||||
3824 | push @openstack, {type => 'paragraph', | ||||||
3825 | name => 'p'}; | ||||||
3826 | $currstyle->{paragraph_type} = 'p'; | ||||||
3827 | $currstyle->{align} = undef; | ||||||
3828 | $currstyle->{indent} = undef; | ||||||
3829 | $res .= " "; |
||||||
3830 | } | ||||||
3831 | $res .= | ||||||
3832 | xml_quote($buf->get_text($buf->get_iter_at_offset($lastpos), | ||||||
3833 | $buf->get_iter_at_offset($tag->{Start}), | ||||||
3834 | 0)); | ||||||
3835 | } | ||||||
3836 | $lastpos = $tag->{End}; | ||||||
3837 | # Auto/singular tags | ||||||
3838 | if (exists $tag->{Tags}{p}) { | ||||||
3839 | # p acts as a paragraph and font terminator - nothing 'matches' it | ||||||
3840 | # ensure any open tags are closed | ||||||
3841 | while (scalar(@openstack)) { | ||||||
3842 | my $this = pop(@openstack); | ||||||
3843 | $res .= "$this->{name}>"; | ||||||
3844 | $res .= "\n" if $this->{type} eq 'paragraph'; | ||||||
3845 | } | ||||||
3846 | my $txt = $buf->get_text($buf->get_iter_at_offset($tag->{Start}), | ||||||
3847 | $buf->get_iter_at_offset($tag->{End}), 0); | ||||||
3848 | $txt =~ s/^\n[^\n]*\n//; | ||||||
3849 | while ($txt =~ s/^[^\n]*\n[^\n]*\n//) { # Spacing paragraphs | ||||||
3850 | $res .= "\n"; | ||||||
3851 | } | ||||||
3852 | $res .= " \n" if $txt =~ /\n/; |
||||||
3853 | $currstyle = {paragraph_type => undef, | ||||||
3854 | indent => undef, | ||||||
3855 | align => undef}; | ||||||
3856 | next; | ||||||
3857 | } elsif (exists $tag->{Tags}{br}) { | ||||||
3858 | $res .= " \n"; |
||||||
3859 | next; | ||||||
3860 | } elsif (exists $tag->{Tags}{ws}) { | ||||||
3861 | $res .= ""; | ||||||
3862 | $res .= | ||||||
3863 | xml_quote($buf->get_text($buf->get_iter_at_offset($tag->{Start}), | ||||||
3864 | $buf->get_iter_at_offset($tag->{End}), 0)); | ||||||
3865 | $res .= ""; | ||||||
3866 | next; | ||||||
3867 | } elsif (exists $tag->{Tags}{asis}) { | ||||||
3868 | # Do as it says! | ||||||
3869 | $res .= $buf->get_text($buf->get_iter_at_offset($tag->{Start}), | ||||||
3870 | $buf->get_iter_at_offset($tag->{End}), 0); | ||||||
3871 | next; | ||||||
3872 | } | ||||||
3873 | # Has our paragraphing changed? If so, close everything. | ||||||
3874 | # For paragraphing changes, we need to know: the para type, the para | ||||||
3875 | # indent and the para alignment. | ||||||
3876 | # Types are p, h1, h2, h3, h4, h5 or undef (undef == no paragraph) | ||||||
3877 | # indent is a number or nothing | ||||||
3878 | # alignment is right, center, fill or nothing | ||||||
3879 | my $newstyle = $class->_get_html_tag_state($tag); | ||||||
3880 | my $changelevel = $class->_get_html_tag_changelevel($newstyle, | ||||||
3881 | $currstyle); | ||||||
3882 | if ($changelevel == CLEV_NONE) { | ||||||
3883 | $res .= | ||||||
3884 | xml_quote($buf->get_text($buf->get_iter_at_offset($tag->{Start}), | ||||||
3885 | $buf->get_iter_at_offset($tag->{End}), 0)); | ||||||
3886 | next; | ||||||
3887 | } | ||||||
3888 | # ROLLBACK! | ||||||
3889 | { | ||||||
3890 | my @stopat; | ||||||
3891 | push @stopat, 'paragraph' if $changelevel < CLEV_PARAGRAPH; | ||||||
3892 | push @stopat, 'pre' if $changelevel < CLEV_PRE; | ||||||
3893 | push @stopat, 'span' if $changelevel < CLEV_SPAN; | ||||||
3894 | push @stopat, ('sub', 'sup') if $changelevel < CLEV_SUPSUB; | ||||||
3895 | while (scalar(@openstack)) { | ||||||
3896 | last if grep {$_ eq $openstack[-1]{type}} @stopat; | ||||||
3897 | my $this = pop(@openstack); | ||||||
3898 | $res .= "$this->{name}>"; | ||||||
3899 | $res .= "\n" if $this->{type} eq 'paragraph'; | ||||||
3900 | } | ||||||
3901 | } | ||||||
3902 | # REAPPLY! | ||||||
3903 | if ($changelevel == CLEV_PARAGRAPH) { | ||||||
3904 | # <(p|h1|h2|h3|h4|h5) style="margin-left:(32 * (X + 1))px; | ||||||
3905 | # text-align:center|right|fill"> | ||||||
3906 | $res .= "<$newstyle->{paragraph_type}"; | ||||||
3907 | my @style; | ||||||
3908 | if (defined($newstyle->{indent})) { | ||||||
3909 | my $dir = 'left'; | ||||||
3910 | $dir = 'right' if $newstyle->{align} eq 'right'; | ||||||
3911 | push @style, ("margin-$dir:" . 32 * ($newstyle->{indent}[0] + 1) . | ||||||
3912 | "px") | ||||||
3913 | } | ||||||
3914 | push @style, "text-align:$newstyle->{align}" | ||||||
3915 | if defined $newstyle->{align}; | ||||||
3916 | $res .= " style=\"" . join(";", @style) . "\"" if scalar(@style); | ||||||
3917 | $res .= ">"; | ||||||
3918 | push @openstack, {type => 'paragraph', | ||||||
3919 | name => $newstyle->{paragraph_type}}; | ||||||
3920 | } | ||||||
3921 | if ($changelevel >= CLEV_PRE and exists $newstyle->{pre}) { | ||||||
3922 | $res .= ""; |
||||||
3923 | push @openstack, {type => 'pre', | ||||||
3924 | name => 'pre'}; | ||||||
3925 | } | ||||||
3926 | if ($changelevel >= CLEV_SPAN) { | ||||||
3927 | my @sstyle = $class->_html_style($newstyle); | ||||||
3928 | if (scalar(@sstyle)) { | ||||||
3929 | $res .= ""; | ||||||
3930 | push @openstack, {type => 'span', | ||||||
3931 | name => 'span'}; | ||||||
3932 | } | ||||||
3933 | } | ||||||
3934 | if ($changelevel >= CLEV_SUPSUB) { | ||||||
3935 | if (exists $newstyle->{superscript}) { | ||||||
3936 | $res .= ""; | ||||||
3937 | push @openstack, {type => 'sup', | ||||||
3938 | name => 'sup'}; | ||||||
3939 | } elsif (exists $newstyle->{subscript}) { | ||||||
3940 | $res .= ""; | ||||||
3941 | push @openstack, {type => 'sub', | ||||||
3942 | name => 'sub'}; | ||||||
3943 | } | ||||||
3944 | } | ||||||
3945 | if ($changelevel >= CLEV_LINK and exists $newstyle->{link}) { | ||||||
3946 | $res .= "{link}) . "\">"; | ||||||
3947 | push @openstack, {type => 'link', | ||||||
3948 | name => 'a'}; | ||||||
3949 | } | ||||||
3950 | $currstyle = $newstyle; | ||||||
3951 | $res .= | ||||||
3952 | xml_quote($buf->get_text($buf->get_iter_at_offset($tag->{Start}), | ||||||
3953 | $buf->get_iter_at_offset($tag->{End}), 0)); | ||||||
3954 | } | ||||||
3955 | my ($s, $e) = ($buf->get_iter_at_offset($tags[-1]{End}), | ||||||
3956 | $buf->get_end_iter); | ||||||
3957 | while (scalar(@openstack)) { | ||||||
3958 | last if not $s->equal($e) and $openstack[-1]{type} eq 'paragraph'; | ||||||
3959 | my $this = pop(@openstack); | ||||||
3960 | $res .= "$this->{name}>"; | ||||||
3961 | $res .= "\n" if $this->{type} eq 'paragraph'; | ||||||
3962 | } | ||||||
3963 | return $res if $s->equal($e); | ||||||
3964 | if (not scalar(@openstack)) { | ||||||
3965 | $res .= " "; |
||||||
3966 | push @openstack, {type => 'paragraph', | ||||||
3967 | name => 'p'}; | ||||||
3968 | } | ||||||
3969 | $res .= xml_quote($buf->get_text($s, $e, 0)); | ||||||
3970 | while (scalar(@openstack)) { | ||||||
3971 | my $this = pop(@openstack); | ||||||
3972 | $res .= "$this->{name}>"; | ||||||
3973 | $res .= "\n" if $this->{type} eq 'paragraph'; | ||||||
3974 | } | ||||||
3975 | return $res; | ||||||
3976 | } | ||||||
3977 | } | ||||||
3978 | |||||||
3979 | 1; | ||||||
3980 | __END__ |