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