File Coverage

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 containing both a text view
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, or through
643             the web interface at L. I will be notified, and then you'll
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/
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 .= "{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 .= "{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 .= "{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 .= "{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 .= "{name}>";
3973             $res .= "\n" if $this->{type} eq 'paragraph';
3974             }
3975             return $res;
3976             }
3977             }
3978            
3979             1;
3980             __END__