1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Parent class for text and flowtext
4 *
5 * Authors:
6 * bulia byak
7 * Richard Hughes
8 * Jon A. Cruz <jon@joncruz.org>
9 * Abhishek Sharma
10 *
11 * Copyright (C) 2004-5 authors
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 */
15
16 #ifdef HAVE_CONFIG_H
17 #endif
18
19 #include <cstring>
20 #include <string>
21 #include <glibmm/i18n.h>
22
23 #include "desktop.h"
24 #include "document.h"
25 #include "inkscape.h"
26 #include "message-stack.h"
27 #include "text-editing.h"
28
29 #include "object/sp-textpath.h"
30 #include "object/sp-flowtext.h"
31 #include "object/sp-flowdiv.h"
32 #include "object/sp-flowregion.h"
33 #include "object/sp-item-group.h"
34 #include "object/sp-tref.h"
35 #include "object/sp-tspan.h"
36 #include "style.h"
37
38 #include "util/units.h"
39
40 #include "xml/attribute-record.h"
41 #include "xml/sp-css-attr.h"
42
43 static const gchar *tref_edit_message = _("You cannot edit <b>cloned character data</b>.");
44 static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend = false);
45
46 static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration);
47
te_get_layout(SPItem const * item)48 Inkscape::Text::Layout const * te_get_layout (SPItem const *item)
49 {
50 if (SP_IS_TEXT(item)) {
51 return &(SP_TEXT(item)->layout);
52 } else if (SP_IS_FLOWTEXT (item)) {
53 return &(SP_FLOWTEXT(item)->layout);
54 }
55 return nullptr;
56 }
57
te_update_layout_now(SPItem * item)58 static void te_update_layout_now (SPItem *item)
59 {
60 if (SP_IS_TEXT(item))
61 SP_TEXT(item)->rebuildLayout();
62 else if (SP_IS_FLOWTEXT (item))
63 SP_FLOWTEXT(item)->rebuildLayout();
64 item->updateRepr();
65 }
66
te_update_layout_now_recursive(SPItem * item)67 void te_update_layout_now_recursive(SPItem *item)
68 {
69 if (SP_IS_GROUP(item)) {
70 std::vector<SPItem*> item_list = sp_item_group_item_list(SP_GROUP(item));
71 for(auto list_item : item_list){
72 te_update_layout_now_recursive(list_item);
73 }
74 } else if (SP_IS_TEXT(item))
75 SP_TEXT(item)->rebuildLayout();
76 else if (SP_IS_FLOWTEXT (item))
77 SP_FLOWTEXT(item)->rebuildLayout();
78 item->updateRepr();
79 }
80
sp_te_output_is_empty(SPItem const * item)81 bool sp_te_output_is_empty(SPItem const *item)
82 {
83 Inkscape::Text::Layout const *layout = te_get_layout(item);
84 return layout->begin() == layout->end();
85 }
86
sp_te_input_is_empty(SPObject const * item)87 bool sp_te_input_is_empty(SPObject const *item)
88 {
89 bool empty = true;
90 if (SP_IS_STRING(item)) {
91 empty = SP_STRING(item)->string.empty();
92 } else {
93 for (auto& child: item->children) {
94 if (!sp_te_input_is_empty(&child)) {
95 empty = false;
96 break;
97 }
98 }
99 }
100 return empty;
101 }
102
103 Inkscape::Text::Layout::iterator
sp_te_get_position_by_coords(SPItem const * item,Geom::Point const & i_p)104 sp_te_get_position_by_coords (SPItem const *item, Geom::Point const &i_p)
105 {
106 Geom::Affine im (item->i2dt_affine ());
107 im = im.inverse();
108
109 Geom::Point p = i_p * im;
110 Inkscape::Text::Layout const *layout = te_get_layout(item);
111 return layout->getNearestCursorPositionTo(p);
112 }
113
sp_te_create_selection_quads(SPItem const * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,Geom::Affine const & transform)114 std::vector<Geom::Point> sp_te_create_selection_quads(SPItem const *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, Geom::Affine const &transform)
115 {
116 if (start == end)
117 return std::vector<Geom::Point>();
118 Inkscape::Text::Layout const *layout = te_get_layout(item);
119 if (layout == nullptr)
120 return std::vector<Geom::Point>();
121
122 return layout->createSelectionShape(start, end, transform);
123 }
124
125 void
sp_te_get_cursor_coords(SPItem const * item,Inkscape::Text::Layout::iterator const & position,Geom::Point & p0,Geom::Point & p1)126 sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, Geom::Point &p0, Geom::Point &p1)
127 {
128 Inkscape::Text::Layout const *layout = te_get_layout(item);
129 double height, rotation;
130 layout->queryCursorShape(position, p0, height, rotation);
131 p1 = Geom::Point(p0[Geom::X] + height * sin(rotation), p0[Geom::Y] - height * cos(rotation)); // valgrind warns that rotation is not initialized here. Why is to be seen in queryCursorShape
132 }
133
sp_te_style_at_position(SPItem const * text,Inkscape::Text::Layout::iterator const & position)134 SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
135 {
136 SPObject const *pos_obj = sp_te_object_at_position(text, position);
137 SPStyle *result = (pos_obj) ? pos_obj->style : nullptr;
138 return result;
139 }
140
sp_te_object_at_position(SPItem const * text,Inkscape::Text::Layout::iterator const & position)141 SPObject const * sp_te_object_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
142 {
143 Inkscape::Text::Layout const *layout = te_get_layout(text);
144 if (layout == nullptr) {
145 return nullptr;
146 }
147 SPObject *rawptr = nullptr;
148 layout->getSourceOfCharacter(position, &rawptr);
149 SPObject const *pos_obj = rawptr;
150 if (pos_obj == nullptr) {
151 pos_obj = text;
152 }
153 while (pos_obj->style == nullptr) {
154 pos_obj = pos_obj->parent; // not interested in SPStrings
155 }
156 return pos_obj;
157 }
158
159 /*
160 * for debugging input
161 *
162 char * dump_hexy(const gchar * utf8)
163 {
164 static char buffer[1024];
165
166 buffer[0]='\0';
167 for (const char *ptr=utf8; *ptr; ptr++) {
168 sprintf(buffer+strlen(buffer),"x%02X",(unsigned char)*ptr);
169 }
170 return buffer;
171 }
172 */
173
sp_te_replace(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,gchar const * utf8)174 Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8)
175 {
176 iterator_pair pair;
177 sp_te_delete(item, start, end, pair);
178 return sp_te_insert(item, pair.first, utf8);
179 }
180
181
182 /* ***************************************************************************************************/
183 // I N S E R T I N G T E X T
184
is_line_break_object(SPObject const * object)185 static bool is_line_break_object(SPObject const *object)
186 {
187 bool is_line_break = false;
188
189 if (object) {
190 if (SP_IS_TEXT(object)
191 || (SP_IS_TSPAN(object) && SP_TSPAN(object)->role != SP_TSPAN_ROLE_UNSPECIFIED)
192 || SP_IS_TEXTPATH(object)
193 || SP_IS_FLOWDIV(object)
194 || SP_IS_FLOWPARA(object)
195 || SP_IS_FLOWLINE(object)
196 || SP_IS_FLOWREGIONBREAK(object)) {
197
198 is_line_break = true;
199 }
200 }
201
202 return is_line_break;
203 }
204
205 /** returns the attributes for an object, or NULL if it isn't a text,
206 tspan, tref, or textpath. */
attributes_for_object(SPObject * object)207 static TextTagAttributes* attributes_for_object(SPObject *object)
208 {
209 if (SP_IS_TSPAN(object))
210 return &SP_TSPAN(object)->attributes;
211 if (SP_IS_TEXT(object))
212 return &SP_TEXT(object)->attributes;
213 if (SP_IS_TREF(object))
214 return &SP_TREF(object)->attributes;
215 if (SP_IS_TEXTPATH(object))
216 return &SP_TEXTPATH(object)->attributes;
217 return nullptr;
218 }
219
span_name_for_text_object(SPObject const * object)220 static const char * span_name_for_text_object(SPObject const *object)
221 {
222 if (SP_IS_TEXT(object)) return "svg:tspan";
223 else if (SP_IS_FLOWTEXT(object)) return "svg:flowSpan";
224 return nullptr;
225 }
226
sp_text_get_length(SPObject const * item)227 unsigned sp_text_get_length(SPObject const *item)
228 {
229 unsigned length = 0;
230
231 if (SP_IS_STRING(item)) {
232 length = SP_STRING(item)->string.length();
233 } else {
234 if (is_line_break_object(item)) {
235 length++;
236 }
237
238 for (auto& child: item->children) {
239 if (SP_IS_STRING(&child)) {
240 length += SP_STRING(&child)->string.length();
241 } else {
242 length += sp_text_get_length(&child);
243 }
244 }
245 }
246
247 return length;
248 }
249
sp_text_get_length_upto(SPObject const * item,SPObject const * upto)250 unsigned sp_text_get_length_upto(SPObject const *item, SPObject const *upto)
251 {
252 unsigned length = 0;
253
254 // The string is the lowest level and the length can be counted directly.
255 if (SP_IS_STRING(item)) {
256 return SP_STRING(item)->string.length();
257 }
258
259 // Take care of new lines...
260 if (is_line_break_object(item) && !SP_IS_TEXT(item)) {
261 if (item != item->parent->firstChild()) {
262 // add 1 for each newline
263 length++;
264 }
265 }
266
267 // Count the length of the children
268 for (auto& child: item->children) {
269 if (upto && &child == upto) {
270 // hit upto, return immediately
271 return length;
272 }
273 if (SP_IS_STRING(&child)) {
274 length += SP_STRING(&child)->string.length();
275 }
276 else {
277 if (upto && child.isAncestorOf(upto)) {
278 // upto is below us, recurse and break loop
279 length += sp_text_get_length_upto(&child, upto);
280 return length;
281 } else {
282 // recurse and go to the next sibling
283 length += sp_text_get_length_upto(&child, upto);
284 }
285 }
286 }
287 return length;
288 }
289
duplicate_node_without_children(Inkscape::XML::Document * xml_doc,Inkscape::XML::Node const * old_node)290 static Inkscape::XML::Node* duplicate_node_without_children(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node const *old_node)
291 {
292 switch (old_node->type()) {
293 case Inkscape::XML::NodeType::ELEMENT_NODE: {
294 Inkscape::XML::Node *new_node = xml_doc->createElement(old_node->name());
295 GQuark const id_key = g_quark_from_string("id");
296 for ( const auto & attr: old_node->attributeList() ) {
297 if (attr.key == id_key) continue;
298 new_node->setAttribute(g_quark_to_string(attr.key), attr.value);
299 }
300 return new_node;
301 }
302
303 case Inkscape::XML::NodeType::TEXT_NODE:
304 return xml_doc->createTextNode(old_node->content());
305
306 case Inkscape::XML::NodeType::COMMENT_NODE:
307 return xml_doc->createComment(old_node->content());
308
309 case Inkscape::XML::NodeType::PI_NODE:
310 return xml_doc->createPI(old_node->name(), old_node->content());
311
312 case Inkscape::XML::NodeType::DOCUMENT_NODE:
313 return nullptr; // this had better never happen
314 }
315 return nullptr;
316 }
317
318 /** returns the sum of the (recursive) lengths of all the SPStrings prior
319 to \a item at the same level. */
sum_sibling_text_lengths_before(SPObject const * item)320 static unsigned sum_sibling_text_lengths_before(SPObject const *item)
321 {
322 unsigned char_index = 0;
323 for (auto& sibling: item->parent->children) {
324 if (&sibling == item) {
325 break;
326 }
327 char_index += sp_text_get_length(&sibling);
328 }
329 return char_index;
330 }
331
332 /** splits the attributes for the first object at the given \a char_index
333 and moves the ones after that point into \a second_item. */
split_attributes(SPObject * first_item,SPObject * second_item,unsigned char_index)334 static void split_attributes(SPObject *first_item, SPObject *second_item, unsigned char_index)
335 {
336 TextTagAttributes *first_attrs = attributes_for_object(first_item);
337 TextTagAttributes *second_attrs = attributes_for_object(second_item);
338 if (first_attrs && second_attrs)
339 first_attrs->split(char_index, second_attrs);
340 }
341
342 /** recursively divides the XML node tree into two objects: the original will
343 contain all objects up to and including \a split_obj and the returned value
344 will be the new leaf which represents the copy of \a split_obj and extends
345 down the tree with new elements all the way to the common root which is the
346 parent of the first line break node encountered.
347 */
split_text_object_tree_at(SPObject * split_obj,unsigned char_index)348 static SPObject* split_text_object_tree_at(SPObject *split_obj, unsigned char_index)
349 {
350 Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
351 if (is_line_break_object(split_obj)) {
352 Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
353 split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
354 Inkscape::GC::release(new_node);
355 split_attributes(split_obj, split_obj->getNext(), char_index);
356 return split_obj->getNext();
357 } else if (!SP_IS_TSPAN(split_obj) &&
358 !SP_IS_FLOWTSPAN(split_obj) &&
359 !SP_IS_STRING(split_obj)) {
360 std::cerr << "split_text_object_tree_at: Illegal split object type! (Illegal document structure.)" << std::endl;
361 return nullptr;
362 }
363
364 unsigned char_count_before = sum_sibling_text_lengths_before(split_obj);
365 SPObject *duplicate_obj = split_text_object_tree_at(split_obj->parent, char_index + char_count_before);
366
367 if (duplicate_obj == nullptr) {
368 // Illegal document structure (no line break object).
369 return nullptr;
370 }
371
372 // copy the split node
373 Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
374 duplicate_obj->getRepr()->appendChild(new_node);
375 Inkscape::GC::release(new_node);
376
377 // sort out the copied attributes (x/y/dx/dy/rotate)
378 split_attributes(split_obj, duplicate_obj->firstChild(), char_index);
379
380 // then move all the subsequent nodes
381 split_obj = split_obj->getNext();
382 while (split_obj) {
383 Inkscape::XML::Node *move_repr = split_obj->getRepr();
384 SPObject *next_obj = split_obj->getNext(); // this is about to become invalidated by removeChild()
385 Inkscape::GC::anchor(move_repr);
386 split_obj->parent->getRepr()->removeChild(move_repr);
387 duplicate_obj->getRepr()->appendChild(move_repr);
388 Inkscape::GC::release(move_repr);
389
390 split_obj = next_obj;
391 }
392 return duplicate_obj->firstChild();
393 }
394
395 /** inserts a new line break at the given position in a text or flowtext
396 object. If the position is in the middle of a span, the XML tree must be
397 chopped in two such that the line can be created at the root of the text
398 element. Returns an iterator pointing just after the inserted break. */
sp_te_insert_line(SPItem * item,Inkscape::Text::Layout::iterator & position)399 Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *item, Inkscape::Text::Layout::iterator &position)
400 {
401 // Disable newlines in a textpath; TODO: maybe on Enter in a textpath, separate it into two
402 // texpaths attached to the same path, with a vertical shift
403 if (SP_IS_TEXT_TEXTPATH (item) || SP_IS_TREF(item))
404 return position;
405
406 Inkscape::Text::Layout const *layout = te_get_layout(item);
407
408 // If this is plain SVG 1.1 text object without a tspan with sodipodi:role="line", we need
409 // to wrap it or our custom line breaking code won't work!
410 bool need_to_wrap = false;
411 SPText* text_object = dynamic_cast<SPText *>(item);
412 if (text_object && !text_object->has_shape_inside() && !text_object->has_inline_size()) {
413
414 need_to_wrap = true;
415 for (auto child : item->childList(false)) {
416 auto tspan = dynamic_cast<SPTSpan *>(child);
417 if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) {
418 // Already wrapped
419 need_to_wrap = false;
420 break;
421 }
422 }
423
424 if (need_to_wrap) {
425
426 // We'll need to rebuild layout, so store character postion:
427 int char_index = layout->iteratorToCharIndex(position);
428
429 // Create wrapping tspan.
430 Inkscape::XML::Node *text_repr = text_object->getRepr();
431 Inkscape::XML::Document *xml_doc = text_repr->document();
432 Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan");
433 new_tspan_repr->setAttribute("sodipodi:role", "line");
434
435 // Move text content to tspan and add tspan to text object.
436 // To do: This moves <desc> and <title> too.
437 move_child_nodes(text_repr, new_tspan_repr);
438 text_repr->appendChild(new_tspan_repr);
439
440 // Need to find new iterator.
441 text_object->rebuildLayout();
442 position = layout->charIndexToIterator(char_index);
443 }
444 }
445
446 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
447
448 SPObject *split_obj = nullptr;
449 Glib::ustring::iterator split_text_iter;
450 if (position != layout->end()) {
451 layout->getSourceOfCharacter(position, &split_obj, &split_text_iter);
452 }
453
454 if (split_obj == nullptr || is_line_break_object(split_obj)) {
455
456 if (split_obj == nullptr) split_obj = item->lastChild();
457
458 if (SP_IS_TREF(split_obj)) {
459 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
460 return position;
461 }
462
463 if (split_obj) {
464 Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
465 Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
466 // if we finaly go to a text element without TSpan we mist set content to none
467 // new_node->setContent("");
468 split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
469 Inkscape::GC::release(new_node);
470 }
471
472 } else if (SP_IS_STRING(split_obj)) {
473
474 // If the parent is a tref, editing on this particular string is disallowed.
475 if (SP_IS_TREF(split_obj->parent)) {
476 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
477 return position;
478 }
479
480 Glib::ustring *string = &SP_STRING(split_obj)->string;
481 unsigned char_index = 0;
482 for (Glib::ustring::iterator it = string->begin() ; it != split_text_iter ; ++it)
483 char_index++;
484 // we need to split the entire text tree into two
485
486 SPObject *object = split_text_object_tree_at(split_obj, char_index);
487 if (object == nullptr) {
488 // Illegal document structure
489 return position;
490 }
491
492 SPString *new_string = SP_STRING(object);
493 new_string->getRepr()->setContent(&*split_text_iter.base()); // a little ugly
494 string->erase(split_text_iter, string->end());
495 split_obj->getRepr()->setContent(string->c_str());
496 // TODO: if the split point was at the beginning of a span we have a whole load of empty elements to clean up
497 } else {
498 // TODO
499 // I think the only case to put here is arbitrary gaps, which nobody uses yet
500 }
501
502 unsigned char_index = layout->iteratorToCharIndex(position);
503 te_update_layout_now(item);
504 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
505 return layout->charIndexToIterator(char_index + 1);
506 }
507
508 /** finds the first SPString after the given position, including children, excluding parents */
sp_te_seek_next_string_recursive(SPObject * start_obj)509 static SPString* sp_te_seek_next_string_recursive(SPObject *start_obj)
510 {
511 while (start_obj) {
512 if (start_obj->hasChildren()) {
513 SPString *found_string = sp_te_seek_next_string_recursive(start_obj->firstChild());
514 if (found_string) {
515 return found_string;
516 }
517 }
518 if (SP_IS_STRING(start_obj)) {
519 return SP_STRING(start_obj);
520 }
521 start_obj = start_obj->getNext();
522 if (is_line_break_object(start_obj)) {
523 break; // don't cross line breaks
524 }
525 }
526 return nullptr;
527 }
528
529 /** inserts the given characters into the given string and inserts
530 corresponding new x/y/dx/dy/rotate attributes into all its parents. */
insert_into_spstring(SPString * string_item,Glib::ustring::iterator iter_at,gchar const * utf8)531 static void insert_into_spstring(SPString *string_item, Glib::ustring::iterator iter_at, gchar const *utf8)
532 {
533 unsigned char_index = 0;
534 unsigned char_count = g_utf8_strlen(utf8, -1);
535 Glib::ustring *string = &SP_STRING(string_item)->string;
536
537 for (Glib::ustring::iterator it = string->begin() ; it != iter_at ; ++it)
538 char_index++;
539 string->replace(iter_at, iter_at, utf8);
540
541 SPObject *parent_item = string_item;
542 for ( ; ; ) {
543 char_index += sum_sibling_text_lengths_before(parent_item);
544 parent_item = parent_item->parent;
545 TextTagAttributes *attributes = attributes_for_object(parent_item);
546 if (!attributes) break;
547 attributes->insert(char_index, char_count);
548 }
549 }
550
551 /** Inserts the given text into a text or flowroot object. Line breaks
552 cannot be inserted using this function, see sp_te_insert_line(). Returns
553 an iterator pointing just after the inserted text. */
554 Inkscape::Text::Layout::iterator
sp_te_insert(SPItem * item,Inkscape::Text::Layout::iterator const & position,gchar const * utf8)555 sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, gchar const *utf8)
556 {
557 if (!g_utf8_validate(utf8,-1,nullptr)) {
558 g_warning("Trying to insert invalid utf8");
559 return position;
560 }
561
562 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
563
564 Inkscape::Text::Layout const *layout = te_get_layout(item);
565 Glib::ustring::iterator iter_text;
566 // we want to insert after the previous char, not before the current char.
567 // it makes a difference at span boundaries
568 Inkscape::Text::Layout::iterator it_prev_char = position;
569 bool cursor_at_start = !it_prev_char.prevCharacter();
570 bool cursor_at_end = position == layout->end();
571 SPObject *source_obj = nullptr;
572 layout->getSourceOfCharacter(it_prev_char, &source_obj, &iter_text);
573 if (SP_IS_STRING(source_obj)) {
574 // If the parent is a tref, editing on this particular string is disallowed.
575 if (SP_IS_TREF(source_obj->parent)) {
576 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
577 return position;
578 }
579
580 // Now the simple case can begin...
581 if (!cursor_at_start){
582 ++iter_text;
583 }
584 SPString *string_item = SP_STRING(source_obj);
585 insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : iter_text, utf8);
586 } else {
587 // the not-so-simple case where we're at a line break or other control char; add to the next child/sibling SPString
588 Inkscape::XML::Document *xml_doc = item->getRepr()->document();
589 if (cursor_at_start) {
590 source_obj = item;
591 if (source_obj->hasChildren()) {
592 source_obj = source_obj->firstChild();
593 if (SP_IS_FLOWTEXT(item)) {
594 while (SP_IS_FLOWREGION(source_obj) || SP_IS_FLOWREGIONEXCLUDE(source_obj)) {
595 source_obj = source_obj->getNext();
596 }
597 if (source_obj == nullptr) {
598 source_obj = item;
599 }
600 }
601 }
602 if (source_obj == item && SP_IS_FLOWTEXT(item)) {
603 Inkscape::XML::Node *para = xml_doc->createElement("svg:flowPara");
604 item->getRepr()->appendChild(para);
605 source_obj = item->lastChild();
606 }
607 } else
608 source_obj = source_obj->getNext();
609
610 if (source_obj) { // never fails
611 SPString *string_item = sp_te_seek_next_string_recursive(source_obj);
612 if (string_item == nullptr) {
613 // need to add an SPString in this (pathological) case
614 Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
615 source_obj->getRepr()->addChild(rstring, nullptr);
616 Inkscape::GC::release(rstring);
617 g_assert(SP_IS_STRING(source_obj->firstChild()));
618 string_item = SP_STRING(source_obj->firstChild());
619 }
620 // If the parent is a tref, editing on this particular string is disallowed.
621 if (SP_IS_TREF(string_item->parent)) {
622 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
623 return position;
624 }
625
626 insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : string_item->string.begin(), utf8);
627 }
628 }
629
630 unsigned char_index = layout->iteratorToCharIndex(position);
631 te_update_layout_now(item);
632 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
633 return layout->charIndexToIterator(char_index + g_utf8_strlen(utf8, -1));
634 }
635
636
637 /* ***************************************************************************************************/
638 // D E L E T I N G T E X T
639
640 /** moves all the children of \a from_repr to \a to_repr, either before
641 the existing children or after them. Order is maintained. The empty
642 \a from_repr is not deleted. */
move_child_nodes(Inkscape::XML::Node * from_repr,Inkscape::XML::Node * to_repr,bool prepend)643 static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend)
644 {
645 while (from_repr->childCount()) {
646 Inkscape::XML::Node *child = prepend ? from_repr->lastChild() : from_repr->firstChild();
647 Inkscape::GC::anchor(child);
648 from_repr->removeChild(child);
649 if (prepend) to_repr->addChild(child, nullptr);
650 else to_repr->appendChild(child);
651 Inkscape::GC::release(child);
652 }
653 }
654
655 /** returns the object in the tree which is the closest ancestor of both
656 \a one and \a two. It will never return anything higher than \a text. */
get_common_ancestor(SPObject * text,SPObject * one,SPObject * two)657 static SPObject* get_common_ancestor(SPObject *text, SPObject *one, SPObject *two)
658 {
659 if (one == nullptr || two == nullptr)
660 return text;
661 SPObject *common_ancestor = one;
662 if (SP_IS_STRING(common_ancestor))
663 common_ancestor = common_ancestor->parent;
664 while (!(common_ancestor == two || common_ancestor->isAncestorOf(two))) {
665 g_assert(common_ancestor != text);
666 common_ancestor = common_ancestor->parent;
667 }
668 return common_ancestor;
669 }
670
671 /** positions \a para_obj and \a text_iter to be pointing at the end
672 of the last string in the last leaf object of \a para_obj. If the last
673 leaf is not an SPString then \a text_iter will be unchanged. */
move_to_end_of_paragraph(SPObject ** para_obj,Glib::ustring::iterator * text_iter)674 static void move_to_end_of_paragraph(SPObject **para_obj, Glib::ustring::iterator *text_iter)
675 {
676 while ((*para_obj)->hasChildren())
677 *para_obj = (*para_obj)->lastChild();
678 if (SP_IS_STRING(*para_obj))
679 *text_iter = SP_STRING(*para_obj)->string.end();
680 }
681
682 /** delete the line break pointed to by \a item by merging its children into
683 the next suitable object and deleting \a item. Returns the object after the
684 ones that have just been moved and sets \a next_is_sibling accordingly. */
delete_line_break(SPObject * root,SPObject * item,bool * next_is_sibling)685 static SPObject* delete_line_break(SPObject *root, SPObject *item, bool *next_is_sibling)
686 {
687 Inkscape::XML::Node *this_repr = item->getRepr();
688 SPObject *next_item = nullptr;
689 unsigned moved_char_count = sp_text_get_length(item) - 1; // the -1 is because it's going to count the line break
690
691 /* some sample cases (the div is the item to be deleted, the * represents where to put the new span):
692 <div></div><p>*text</p>
693 <p><div></div>*text</p>
694 <p><div></div></p><p>*text</p>
695 */
696 Inkscape::XML::Document *xml_doc = item->getRepr()->document();
697 Inkscape::XML::Node *new_span_repr = xml_doc->createElement(span_name_for_text_object(root));
698
699 new_span_repr->setAttributeOrRemoveIfEmpty("dx", this_repr->attribute("dx"));
700 new_span_repr->setAttributeOrRemoveIfEmpty("dy", this_repr->attribute("dy"));
701 new_span_repr->setAttributeOrRemoveIfEmpty("rotate", this_repr->attribute("rotate"));
702
703 SPObject *following_item = item;
704 while (following_item->getNext() == nullptr) {
705 following_item = following_item->parent;
706 g_assert(following_item != root);
707 }
708 following_item = following_item->getNext();
709
710 SPObject *new_parent_item;
711 if (SP_IS_STRING(following_item)) {
712 new_parent_item = following_item->parent;
713 new_parent_item->getRepr()->addChild(new_span_repr, following_item->getPrev() ? following_item->getPrev()->getRepr() : nullptr);
714 next_item = following_item;
715 *next_is_sibling = true;
716 } else {
717 new_parent_item = following_item;
718 next_item = new_parent_item->firstChild();
719 *next_is_sibling = true;
720 if (next_item == nullptr) {
721 next_item = new_parent_item;
722 *next_is_sibling = false;
723 }
724 new_parent_item->getRepr()->addChild(new_span_repr, nullptr);
725 }
726
727 // work around a bug in sp_style_write_difference() which causes the difference
728 // not to be written if the second param has a style set which the first does not
729 // by causing the first param to have everything set
730 SPCSSAttr *dest_node_attrs = sp_repr_css_attr(new_parent_item->getRepr(), "style");
731 SPCSSAttr *this_node_attrs = sp_repr_css_attr(this_repr, "style");
732 SPCSSAttr *this_node_attrs_inherited = sp_repr_css_attr_inherited(this_repr, "style");
733 for ( const auto & attr :dest_node_attrs->attributeList()) {
734 gchar const *key = g_quark_to_string(attr.key);
735 gchar const *this_attr = this_node_attrs_inherited->attribute(key);
736 if ((this_attr == nullptr || strcmp(attr.value, this_attr)) && this_node_attrs->attribute(key) == nullptr)
737 this_node_attrs->setAttribute(key, this_attr);
738 }
739 sp_repr_css_attr_unref(this_node_attrs_inherited);
740 sp_repr_css_attr_unref(this_node_attrs);
741 sp_repr_css_attr_unref(dest_node_attrs);
742 sp_repr_css_change(new_span_repr, this_node_attrs, "style");
743
744 TextTagAttributes *attributes = attributes_for_object(new_parent_item);
745 if (attributes)
746 attributes->insert(0, moved_char_count);
747 move_child_nodes(this_repr, new_span_repr);
748 this_repr->parent()->removeChild(this_repr);
749 return next_item;
750 }
751
752 /** erases the given characters from the given string and deletes the
753 corresponding x/y/dx/dy/rotate attributes from all its parents. */
erase_from_spstring(SPString * string_item,Glib::ustring::iterator iter_from,Glib::ustring::iterator iter_to)754 static void erase_from_spstring(SPString *string_item, Glib::ustring::iterator iter_from, Glib::ustring::iterator iter_to)
755 {
756 unsigned char_index = 0;
757 unsigned char_count = 0;
758 Glib::ustring *string = &SP_STRING(string_item)->string;
759
760 for (Glib::ustring::iterator it = string->begin() ; it != iter_from ; ++it){
761 char_index++;
762 }
763 for (Glib::ustring::iterator it = iter_from ; it != iter_to ; ++it){
764 char_count++;
765 }
766 string->erase(iter_from, iter_to);
767 string_item->getRepr()->setContent(string->c_str());
768
769 SPObject *parent_item = string_item;
770 for ( ; ; ) {
771 char_index += sum_sibling_text_lengths_before(parent_item);
772 parent_item = parent_item->parent;
773 TextTagAttributes *attributes = attributes_for_object(parent_item);
774 if (attributes == nullptr) {
775 break;
776 }
777
778 attributes->erase(char_index, char_count);
779 attributes->writeTo(parent_item->getRepr());
780 }
781 }
782
783 /* Deletes the given characters from a text or flowroot object. This is
784 quite a complicated operation, partly due to the cleanup that is done if all
785 the text in a subobject has been deleted, and partly due to the difficulty
786 of figuring out what is a line break and how to delete one. Returns the
787 real start and ending iterators based on the situation. */
788 bool
sp_te_delete(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,iterator_pair & iter_pair)789 sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start,
790 Inkscape::Text::Layout::iterator const &end, iterator_pair &iter_pair)
791 {
792 bool success = false;
793
794 iter_pair.first = start;
795 iter_pair.second = end;
796
797 if (start == end) return success;
798
799 if (start > end) {
800 iter_pair.first = end;
801 iter_pair.second = start;
802 }
803
804 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
805
806 Inkscape::Text::Layout const *layout = te_get_layout(item);
807 SPObject *start_item = nullptr, *end_item = nullptr;
808 Glib::ustring::iterator start_text_iter, end_text_iter;
809 layout->getSourceOfCharacter(iter_pair.first, &start_item, &start_text_iter);
810 layout->getSourceOfCharacter(iter_pair.second, &end_item, &end_text_iter);
811 if (start_item == nullptr) {
812 return success; // start is at end of text
813 }
814 if (is_line_break_object(start_item)) {
815 move_to_end_of_paragraph(&start_item, &start_text_iter);
816 }
817 if (end_item == nullptr) {
818 end_item = item->lastChild();
819 move_to_end_of_paragraph(&end_item, &end_text_iter);
820 } else if (is_line_break_object(end_item)) {
821 move_to_end_of_paragraph(&end_item, &end_text_iter);
822 }
823
824 SPObject *common_ancestor = get_common_ancestor(item, start_item, end_item);
825
826 bool has_text_decoration = false;
827 gchar const *root_style = (item)->getRepr()->attribute("style");
828 if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
829
830 if (start_item == end_item) {
831 // the quick case where we're deleting stuff all from the same string
832 if (SP_IS_STRING(start_item)) { // always true (if it_start != it_end anyway)
833 // If the parent is a tref, editing on this particular string is disallowed.
834 if (SP_IS_TREF(start_item->parent)) {
835 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
836 } else {
837 erase_from_spstring(SP_STRING(start_item), start_text_iter, end_text_iter);
838 success = true;
839 }
840 }
841 } else {
842 SPObject *sub_item = start_item;
843 // walk the tree from start_item to end_item, deleting as we go
844 while (sub_item != item) {
845 if (sub_item == end_item) {
846 if (SP_IS_STRING(sub_item)) {
847 // If the parent is a tref, editing on this particular string is disallowed.
848 if (SP_IS_TREF(sub_item->parent)) {
849 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
850 break;
851 }
852
853 Glib::ustring *string = &SP_STRING(sub_item)->string;
854 erase_from_spstring(SP_STRING(sub_item), string->begin(), end_text_iter);
855 success = true;
856 }
857 break;
858 }
859 if (SP_IS_STRING(sub_item)) {
860 SPString *string = SP_STRING(sub_item);
861 if (sub_item == start_item)
862 erase_from_spstring(string, start_text_iter, string->string.end());
863 else
864 erase_from_spstring(string, string->string.begin(), string->string.end());
865 success = true;
866 }
867 // walk to the next item in the tree
868 if (sub_item->hasChildren())
869 sub_item = sub_item->firstChild();
870 else {
871 SPObject *next_item;
872 do {
873 bool is_sibling = true;
874 next_item = sub_item->getNext();
875 if (next_item == nullptr) {
876 next_item = sub_item->parent;
877 is_sibling = false;
878 }
879
880 if (is_line_break_object(sub_item))
881 next_item = delete_line_break(item, sub_item, &is_sibling);
882
883 sub_item = next_item;
884 if (is_sibling) break;
885 // no more siblings, go up a parent
886 } while (sub_item != item && sub_item != end_item);
887 }
888 }
889 }
890
891 while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
892 te_update_layout_now(item);
893 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
894 layout->validateIterator(&iter_pair.first);
895 layout->validateIterator(&iter_pair.second);
896 return success;
897 }
898
899
900 /* ***************************************************************************************************/
901 // P L A I N T E X T F U N C T I O N S
902
903 /** Gets a text-only representation of the given text or flowroot object,
904 replacing line break elements with '\n'. */
sp_te_get_ustring_multiline(SPObject const * root,Glib::ustring * string,bool * pending_line_break)905 static void sp_te_get_ustring_multiline(SPObject const *root, Glib::ustring *string, bool *pending_line_break)
906 {
907 if (*pending_line_break) {
908 *string += '\n';
909 }
910 for (auto& child: root->children) {
911 if (SP_IS_STRING(&child)) {
912 *string += SP_STRING(&child)->string;
913 } else {
914 sp_te_get_ustring_multiline(&child, string, pending_line_break);
915 }
916 }
917 if (!SP_IS_TEXT(root) && !SP_IS_TEXTPATH(root) && is_line_break_object(root)) {
918 *pending_line_break = true;
919 }
920 }
921
922 /** Gets a text-only representation of the given text or flowroot object,
923 replacing line break elements with '\n'. The return value must be free()d. */
924 Glib::ustring
sp_te_get_string_multiline(SPItem const * text)925 sp_te_get_string_multiline (SPItem const *text)
926 {
927 Glib::ustring string;
928 bool pending_line_break = false;
929
930 if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) {
931 sp_te_get_ustring_multiline(text, &string, &pending_line_break);
932 }
933 return string;
934 }
935
936 /** Gets a text-only representation of the characters in a text or flowroot
937 object from \a start to \a end only. Line break elements are replaced with
938 '\n'. */
939 Glib::ustring
sp_te_get_string_multiline(SPItem const * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end)940 sp_te_get_string_multiline (SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end)
941 {
942 if (start == end) return "";
943 Inkscape::Text::Layout::iterator first, last;
944 if (start < end) {
945 first = start;
946 last = end;
947 } else {
948 first = end;
949 last = start;
950 }
951 Inkscape::Text::Layout const *layout = te_get_layout(text);
952 Glib::ustring result;
953 // not a particularly fast piece of code. I'll optimise it if people start to notice.
954 for ( ; first < last ; first.nextCharacter()) {
955 SPObject *char_item = nullptr;
956 Glib::ustring::iterator text_iter;
957 layout->getSourceOfCharacter(first, &char_item, &text_iter);
958 if (SP_IS_STRING(char_item)) {
959 result += *text_iter;
960 } else {
961 result += '\n';
962 }
963 }
964 return result;
965 }
966
967 void
sp_te_set_repr_text_multiline(SPItem * text,gchar const * str)968 sp_te_set_repr_text_multiline(SPItem *text, gchar const *str)
969 {
970 g_return_if_fail (text != nullptr);
971 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
972
973 Inkscape::XML::Document *xml_doc = text->getRepr()->document();
974 Inkscape::XML::Node *repr;
975 SPObject *object;
976 bool is_textpath = false;
977 if (SP_IS_TEXT_TEXTPATH (text)) {
978 repr = text->firstChild()->getRepr();
979 object = text->firstChild();
980 is_textpath = true;
981 } else {
982 repr = text->getRepr();
983 object = text;
984 }
985
986 if (!str) {
987 str = "";
988 }
989 gchar *content = g_strdup (str);
990
991 repr->setContent("");
992 for (auto& child: object->childList(false)) {
993 if (!SP_IS_FLOWREGION(child) && !SP_IS_FLOWREGIONEXCLUDE(child)) {
994 repr->removeChild(child->getRepr());
995 }
996 }
997
998 if (is_textpath) {
999 gchar *p = content;
1000 while (*p != '\0') {
1001 if (*p == '\n') {
1002 *p = ' '; // No lines for textpath, replace newlines with spaces.
1003 }
1004 ++p;
1005 }
1006 Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
1007 repr->addChild(rstr, nullptr);
1008 Inkscape::GC::release(rstr);
1009 } else {
1010 SPText* sptext = dynamic_cast<SPText *>(text);
1011 if (sptext && (sptext->has_inline_size() || sptext->has_shape_inside())) {
1012 // Do nothing... we respect newlines (and assume CSS already set to do so).
1013 Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
1014 repr->addChild(rstr, nullptr);
1015 Inkscape::GC::release(rstr);
1016 } else {
1017 // Break into tspans with sodipodi:role="line".
1018 gchar *p = content;
1019 while (p) {
1020 gchar *e = strchr (p, '\n');
1021 if (e) *e = '\0';
1022 Inkscape::XML::Node *rtspan;
1023 if (SP_IS_TEXT(text)) { // create a tspan for each line
1024 rtspan = xml_doc->createElement("svg:tspan");
1025 rtspan->setAttribute("sodipodi:role", "line");
1026 } else { // create a flowPara for each line
1027 rtspan = xml_doc->createElement("svg:flowPara");
1028 }
1029 Inkscape::XML::Node *rstr = xml_doc->createTextNode(p);
1030 rtspan->addChild(rstr, nullptr);
1031 Inkscape::GC::release(rstr);
1032 repr->appendChild(rtspan);
1033 Inkscape::GC::release(rtspan);
1034
1035 p = (e) ? e + 1 : nullptr;
1036 }
1037 }
1038 }
1039 g_free (content);
1040 }
1041
1042 /* ***************************************************************************************************/
1043 // K E R N I N G A N D S P A C I N G
1044
1045 /** Returns the attributes block and the character index within that block
1046 which represents the iterator \a position. */
1047 TextTagAttributes*
text_tag_attributes_at_position(SPItem * item,Inkscape::Text::Layout::iterator const & position,unsigned * char_index)1048 text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index)
1049 {
1050 if (item == nullptr || char_index == nullptr || !SP_IS_TEXT(item)) {
1051 return nullptr; // flowtext doesn't support kerning yet
1052 }
1053 SPText *text = SP_TEXT(item);
1054
1055 SPObject *source_item = nullptr;
1056 Glib::ustring::iterator source_text_iter;
1057 text->layout.getSourceOfCharacter(position, &source_item, &source_text_iter);
1058
1059 if (!SP_IS_STRING(source_item)) {
1060 return nullptr;
1061 }
1062 Glib::ustring *string = &SP_STRING(source_item)->string;
1063 *char_index = sum_sibling_text_lengths_before(source_item);
1064 for (Glib::ustring::iterator it = string->begin() ; it != source_text_iter ; ++it) {
1065 ++*char_index;
1066 }
1067
1068 return attributes_for_object(source_item->parent);
1069 }
1070
1071 void
sp_te_adjust_kerning_screen(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop * desktop,Geom::Point by)1072 sp_te_adjust_kerning_screen (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, Geom::Point by)
1073 {
1074 // divide increment by zoom
1075 // divide increment by matrix expansion
1076 gdouble factor = 1 / desktop->current_zoom();
1077 Geom::Affine t (item->i2doc_affine());
1078 factor = factor / t.descrim();
1079 by = factor * by;
1080
1081 unsigned char_index;
1082 TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1083 if (attributes) attributes->addToDxDy(char_index, by);
1084 if (start != end) {
1085 attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1086 if (attributes) attributes->addToDxDy(char_index, -by);
1087 }
1088
1089 item->updateRepr();
1090 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1091 }
1092
sp_te_adjust_dx(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,double delta)1093 void sp_te_adjust_dx(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
1094 {
1095 unsigned char_index = 0;
1096 TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1097 if (attributes) {
1098 attributes->addToDx(char_index, delta);
1099 }
1100 if (start != end) {
1101 attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1102 if (attributes) {
1103 attributes->addToDx(char_index, -delta);
1104 }
1105 }
1106
1107 item->updateRepr();
1108 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1109 }
1110
sp_te_adjust_dy(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,double delta)1111 void sp_te_adjust_dy(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
1112 {
1113 unsigned char_index = 0;
1114 TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1115 if (attributes) {
1116 attributes->addToDy(char_index, delta);
1117 }
1118 if (start != end) {
1119 attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1120 if (attributes) {
1121 attributes->addToDy(char_index, -delta);
1122 }
1123 }
1124
1125 item->updateRepr();
1126 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1127 }
1128
1129 void
sp_te_adjust_rotation_screen(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop * desktop,gdouble pixels)1130 sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble pixels)
1131 {
1132 // divide increment by zoom
1133 // divide increment by matrix expansion
1134 gdouble factor = 1 / desktop->current_zoom();
1135 Geom::Affine t (text->i2doc_affine());
1136 factor = factor / t.descrim();
1137 Inkscape::Text::Layout const *layout = te_get_layout(text);
1138 if (layout == nullptr) return;
1139 SPObject *source_item = nullptr;
1140 layout->getSourceOfCharacter(std::min(start, end), &source_item);
1141 if (source_item == nullptr) {
1142 return;
1143 }
1144 gdouble degrees = (180/M_PI) * atan2(pixels, source_item->parent->style->font_size.computed / factor);
1145
1146 sp_te_adjust_rotation(text, start, end, desktop, degrees);
1147 }
1148
1149 void
sp_te_adjust_rotation(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,gdouble degrees)1150 sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
1151 {
1152 unsigned char_index;
1153 TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
1154 if (attributes == nullptr) return;
1155
1156 if (start != end) {
1157 for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
1158 attributes = text_tag_attributes_at_position(text, it, &char_index);
1159 if (attributes) attributes->addToRotate(char_index, degrees);
1160 }
1161 } else
1162 attributes->addToRotate(char_index, degrees);
1163
1164 text->updateRepr();
1165 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1166 }
1167
sp_te_set_rotation(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,gdouble degrees)1168 void sp_te_set_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
1169 {
1170 unsigned char_index = 0;
1171 TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
1172 if (attributes != nullptr) {
1173 if (start != end) {
1174 for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
1175 attributes = text_tag_attributes_at_position(text, it, &char_index);
1176 if (attributes) {
1177 attributes->setRotate(char_index, degrees);
1178 }
1179 }
1180 } else {
1181 attributes->setRotate(char_index, degrees);
1182 }
1183
1184 text->updateRepr();
1185 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1186 }
1187 }
1188
1189 void
sp_te_adjust_tspan_letterspacing_screen(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop * desktop,gdouble by)1190 sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
1191 {
1192 g_return_if_fail (text != nullptr);
1193 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
1194
1195 Inkscape::Text::Layout const *layout = te_get_layout(text);
1196
1197 gdouble val;
1198 SPObject *source_obj = nullptr;
1199 unsigned nb_let;
1200 layout->getSourceOfCharacter(std::min(start, end), &source_obj);
1201
1202 if (source_obj == nullptr) { // end of text
1203 source_obj = text->lastChild();
1204 }
1205 if (SP_IS_STRING(source_obj)) {
1206 source_obj = source_obj->parent;
1207 }
1208
1209 SPStyle *style = source_obj->style;
1210
1211 // calculate real value
1212 /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and
1213 get rid of the `else val = 0.0'. Similarly below and in sp-string.cpp. */
1214 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
1215 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
1216 val = style->font_size.computed * style->letter_spacing.value;
1217 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
1218 val = style->font_size.computed * style->letter_spacing.value * 0.5;
1219 } else { // unknown unit - should not happen
1220 val = 0.0;
1221 }
1222 } else { // there's a real value in .computed, or it's zero
1223 val = style->letter_spacing.computed;
1224 }
1225
1226 if (start == end) {
1227 while (!is_line_break_object(source_obj)) { // move up the tree so we apply to the closest paragraph
1228 source_obj = source_obj->parent;
1229 }
1230 nb_let = sp_text_get_length(source_obj);
1231 } else {
1232 nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start));
1233 }
1234
1235 // divide increment by zoom and by the number of characters in the line,
1236 // so that the entire line is expanded by by pixels, no matter what its length
1237 gdouble const zoom = desktop->current_zoom();
1238 gdouble const zby = (by
1239 / (zoom * (nb_let > 1 ? nb_let - 1 : 1))
1240 / SP_ITEM(source_obj)->i2doc_affine().descrim());
1241 val += zby;
1242
1243 if (start == end) {
1244 // set back value to entire paragraph
1245 style->letter_spacing.normal = FALSE;
1246 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
1247 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
1248 style->letter_spacing.value = val / style->font_size.computed;
1249 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
1250 style->letter_spacing.value = val / style->font_size.computed * 2;
1251 }
1252 } else {
1253 style->letter_spacing.computed = val;
1254 }
1255
1256 style->letter_spacing.set = TRUE;
1257 } else {
1258 // apply to selection only
1259 SPCSSAttr *css = sp_repr_css_attr_new();
1260 char string_val[40];
1261 g_snprintf(string_val, sizeof(string_val), "%f", val);
1262 sp_repr_css_set_property(css, "letter-spacing", string_val);
1263 sp_te_apply_style(text, start, end, css);
1264 sp_repr_css_attr_unref(css);
1265 }
1266
1267 text->updateRepr();
1268 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1269 }
1270
1271 // Only used for page-up and page-down and sp_te_adjust_linespacing_screen
1272 double
sp_te_get_average_linespacing(SPItem * text)1273 sp_te_get_average_linespacing (SPItem *text)
1274 {
1275 Inkscape::Text::Layout const *layout = te_get_layout(text);
1276 if (!layout)
1277 return 0;
1278
1279 unsigned line_count = layout->lineIndex(layout->end());
1280 double all_lines_height = layout->characterAnchorPoint(layout->end())[Geom::Y] - layout->characterAnchorPoint(layout->begin())[Geom::Y];
1281 double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count);
1282 return average_line_height;
1283 }
1284
1285 /** Adjust the line height by 'amount'.
1286 * If top_level is true then objects without 'line-height' set or withwill get a set value,
1287 * otherwise objects that inherit line-height will not get onw=e.
1288 */
1289 void
sp_te_adjust_line_height(SPObject * object,double amount,double average,bool top_level=true)1290 sp_te_adjust_line_height (SPObject *object, double amount, double average, bool top_level = true) {
1291
1292 SPStyle *style = object->style;
1293
1294 // Always set if top level true.
1295 // Also set if line_height is set to a non-zero value.
1296 if (top_level ||
1297 (style->line_height.set && !style->line_height.inherit && style->line_height.computed != 0)){
1298
1299 // Scale default values
1300 if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) {
1301 style->line_height.set = TRUE;
1302 style->line_height.inherit = FALSE;
1303 style->line_height.normal = FALSE;
1304 style->line_height.unit = SP_CSS_UNIT_NONE;
1305 style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL;
1306 }
1307
1308 switch (style->line_height.unit) {
1309
1310 case SP_CSS_UNIT_NONE:
1311 default:
1312 // Multiplier-type units, stored in computed
1313 if (fabs(style->line_height.computed) < 0.001) {
1314 style->line_height.computed = amount < 0.0 ? -0.001 : 0.001;
1315 // the formula below could get stuck at zero
1316 } else {
1317 style->line_height.computed *= (average + amount) / average;
1318 }
1319 style->line_height.value = style->line_height.computed;
1320 break;
1321
1322
1323 // Relative units, stored in value
1324 case SP_CSS_UNIT_EM:
1325 case SP_CSS_UNIT_EX:
1326 case SP_CSS_UNIT_PERCENT:
1327 if (fabs(style->line_height.value) < 0.001) {
1328 style->line_height.value = amount < 0.0 ? -0.001 : 0.001;
1329 } else {
1330 style->line_height.value *= (average + amount) / average;
1331 }
1332 break;
1333
1334
1335 // Absolute units
1336 case SP_CSS_UNIT_PX:
1337 style->line_height.computed += amount;
1338 style->line_height.value = style->line_height.computed;
1339 break;
1340 case SP_CSS_UNIT_PT:
1341 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pt");
1342 style->line_height.value = style->line_height.computed;
1343 break;
1344 case SP_CSS_UNIT_PC:
1345 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pc");
1346 style->line_height.value = style->line_height.computed;
1347 break;
1348 case SP_CSS_UNIT_MM:
1349 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "mm");
1350 style->line_height.value = style->line_height.computed;
1351 break;
1352 case SP_CSS_UNIT_CM:
1353 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "cm");
1354 style->line_height.value = style->line_height.computed;
1355 break;
1356 case SP_CSS_UNIT_IN:
1357 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "in");
1358 style->line_height.value = style->line_height.computed;
1359 break;
1360 }
1361 object->updateRepr();
1362 }
1363
1364 std::vector<SPObject*> children = object->childList(false);
1365 for (auto child: children) {
1366 sp_te_adjust_line_height (child, amount, average, false);
1367 }
1368 }
1369
1370 void
sp_te_adjust_linespacing_screen(SPItem * text,Inkscape::Text::Layout::iterator const &,Inkscape::Text::Layout::iterator const &,SPDesktop * desktop,gdouble by)1371 sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &/*start*/, Inkscape::Text::Layout::iterator const &/*end*/, SPDesktop *desktop, gdouble by)
1372 {
1373 // TODO: use start and end iterators to delineate the area to be affected
1374 g_return_if_fail (text != nullptr);
1375 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
1376
1377 Inkscape::Text::Layout const *layout = te_get_layout(text);
1378
1379 double average_line_height = sp_te_get_average_linespacing (text);
1380 if (fabs(average_line_height) < 0.001) average_line_height = 0.001;
1381
1382 // divide increment by zoom and by the number of lines,
1383 // so that the entire object is expanded by by pixels
1384 unsigned line_count = layout->lineIndex(layout->end());
1385 gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count));
1386
1387 // divide increment by matrix expansion
1388 Geom::Affine t(text->i2doc_affine());
1389 zby = zby / t.descrim();
1390
1391 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1392 gint mode = prefs->getInt("/tools/text/line_spacing_mode", 0);
1393 if (mode == 0) { // Adaptive: <text> line-spacing is zero, only scale children.
1394 std::vector<SPObject*> children = text->childList(false);
1395 for (auto child: children) {
1396 sp_te_adjust_line_height (child, zby, average_line_height, false);
1397 }
1398 } else {
1399 sp_te_adjust_line_height (text, zby, average_line_height, true);
1400 }
1401
1402 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1403 }
1404
1405
1406 /* ***************************************************************************************************/
1407 // S T Y L E A P P L I C A T I O N
1408
1409
1410 /** converts an iterator to a character index, mainly because ustring::substr()
1411 doesn't have a version that takes iterators as parameters. */
char_index_of_iterator(Glib::ustring const & string,Glib::ustring::const_iterator text_iter)1412 static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter)
1413 {
1414 unsigned n = 0;
1415 for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; ++it)
1416 n++;
1417 return n;
1418 }
1419
1420 // Move to style.h?
1421 /** applies the given style string on top of the existing styles for \a item,
1422 as opposed to sp_style_merge_from_style_string which merges its parameter
1423 underneath the existing styles (ie ignoring already set properties). */
overwrite_style_with_string(SPObject * item,gchar const * style_string)1424 static void overwrite_style_with_string(SPObject *item, gchar const *style_string)
1425 {
1426 SPStyle style(item->document);
1427 style.mergeString(style_string);
1428 gchar const *item_style_string = item->getRepr()->attribute("style");
1429 if (item_style_string && *item_style_string) {
1430 style.mergeString(item_style_string);
1431 }
1432 Glib::ustring new_style_string = style.write();
1433 item->setAttributeOrRemoveIfEmpty("style", new_style_string);
1434 }
1435
1436 // Move to style.h?
1437 /** Returns true if the style of \a parent and the style of \a child are
1438 equivalent (and hence the children of both will appear the same). It is a
1439 limitation of the current implementation that \a parent must be a (not
1440 necessarily immediate) ancestor of \a child. */
objects_have_equal_style(SPObject const * parent,SPObject const * child)1441 static bool objects_have_equal_style(SPObject const *parent, SPObject const *child)
1442 {
1443 // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child))
1444 // will not work. Firstly because of an inheritance bug in style.cpp that has
1445 // implications too large for me to feel safe fixing, but mainly because the css spec
1446 // requires that the computed value is inherited, not the specified value.
1447 g_assert(parent->isAncestorOf(child));
1448
1449 Glib::ustring parent_style = parent->style->write( SP_STYLE_FLAG_ALWAYS );
1450
1451 // we have to write parent_style then read it again, because some properties format their values
1452 // differently depending on whether they're set or not (*cough*dash-offset*cough*)
1453 SPStyle parent_spstyle(parent->document);
1454 parent_spstyle.mergeString(parent_style.c_str());
1455 parent_style = parent_spstyle.write(SP_STYLE_FLAG_ALWAYS);
1456
1457 Glib::ustring child_style_construction;
1458 while (child != parent) {
1459 // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes!
1460 char const *style_text = child->getRepr()->attribute("style");
1461 if (style_text && *style_text) {
1462 child_style_construction.insert(0, style_text);
1463 child_style_construction.insert(0, 1, ';');
1464 }
1465 child = child->parent;
1466 }
1467 child_style_construction.insert(0, parent_style);
1468
1469 SPStyle child_spstyle(parent->document);
1470 child_spstyle.mergeString(child_style_construction.c_str());
1471 Glib::ustring child_style = child_spstyle.write(SP_STYLE_FLAG_ALWAYS);
1472
1473 bool equal = (child_style == parent_style); // Glib::ustring overloads == operator
1474 return equal;
1475 }
1476
1477 /** returns true if \a first and \a second contain all the same attributes
1478 with the same values as each other. Note that we have to compare both
1479 forwards and backwards to make sure we don't miss any attributes that are
1480 in one but not the other. */
css_attrs_are_equal(SPCSSAttr const * first,SPCSSAttr const * second)1481 static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
1482 {
1483 for ( const auto & attr : first->attributeList()) {
1484 gchar const *other_attr = second->attribute(g_quark_to_string(attr.key));
1485 if (other_attr == nullptr || strcmp(attr.value, other_attr))
1486 return false;
1487 }
1488 for (const auto & attr : second->attributeList()) {
1489 gchar const *other_attr = first->attribute(g_quark_to_string(attr.key));
1490 if (other_attr == nullptr || strcmp(attr.value, other_attr))
1491 return false;
1492 }
1493 return true;
1494 }
1495
1496 /** sets the given css attribute on this object and all its descendants.
1497 Annoyingly similar to sp_desktop_apply_css_recursive(), except without the
1498 transform stuff. */
apply_css_recursive(SPObject * o,SPCSSAttr const * css)1499 static void apply_css_recursive(SPObject *o, SPCSSAttr const *css)
1500 {
1501 sp_repr_css_change(o->getRepr(), const_cast<SPCSSAttr*>(css), "style");
1502
1503 for (auto& child: o->children) {
1504 if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", nullptr) != nullptr) {
1505 // Unset properties which are accumulating and thus should not be set recursively.
1506 // For example, setting opacity 0.5 on a group recursively would result in the visible opacity of 0.25 for an item in the group.
1507 SPCSSAttr *css_recurse = sp_repr_css_attr_new();
1508 sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css));
1509 sp_repr_css_set_property(css_recurse, "opacity", nullptr);
1510 apply_css_recursive(&child, css_recurse);
1511 sp_repr_css_attr_unref(css_recurse);
1512 } else {
1513 apply_css_recursive(&child, const_cast<SPCSSAttr*>(css));
1514 }
1515 }
1516 }
1517
1518 /** applies the given style to all the objects at the given level and below
1519 which are between \a start_item and \a end_item, creating spans as necessary.
1520 If \a start_item or \a end_item are NULL then the style is applied to all
1521 objects to the beginning or end respectively. \a span_object_name is the
1522 name of the xml for a text span (ie tspan or flowspan). */
recursively_apply_style(SPObject * common_ancestor,SPCSSAttr const * css,SPObject * start_item,Glib::ustring::iterator start_text_iter,SPObject * end_item,Glib::ustring::iterator end_text_iter,char const * span_object_name)1523 static void recursively_apply_style(SPObject *common_ancestor, SPCSSAttr const *css, SPObject *start_item, Glib::ustring::iterator start_text_iter, SPObject *end_item, Glib::ustring::iterator end_text_iter, char const *span_object_name)
1524 {
1525 bool passed_start = start_item == nullptr ? true : false;
1526 Inkscape::XML::Document *xml_doc = common_ancestor->document->getReprDoc();
1527
1528 for (SPObject *child = common_ancestor->firstChild() ; child ; child = child->getNext()) {
1529 if (start_item == child) {
1530 passed_start = true;
1531 }
1532
1533 if (passed_start) {
1534 if (end_item && child->isAncestorOf(end_item)) {
1535 recursively_apply_style(child, css, nullptr, start_text_iter, end_item, end_text_iter, span_object_name);
1536 break;
1537 }
1538 // apply style
1539
1540 // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working.
1541 // often this means that new spans are created before child and child is modified only
1542 if (SP_IS_STRING(child)) {
1543 SPString *string_item = SP_STRING(child);
1544 bool surround_entire_string = true;
1545
1546 Inkscape::XML::Node *child_span = xml_doc->createElement(span_object_name);
1547 sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style"); // better hope that prototype wasn't nonconst for a good reason
1548 SPObject *prev_item = child->getPrev();
1549 Inkscape::XML::Node *prev_repr = prev_item ? prev_item->getRepr() : nullptr;
1550
1551 if (child == start_item || child == end_item) {
1552 surround_entire_string = false;
1553 if (start_item == end_item && start_text_iter != string_item->string.begin()) {
1554 // eg "abcDEFghi" -> "abc"<span>"DEF"</span>"ghi"
1555 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1556 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1557
1558 Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
1559 common_ancestor->getRepr()->addChild(text_before, prev_repr);
1560 common_ancestor->getRepr()->addChild(child_span, text_before);
1561 Inkscape::GC::release(text_before);
1562 Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index, end_char_index - start_char_index).c_str());
1563 child_span->appendChild(text_in_span);
1564 Inkscape::GC::release(text_in_span);
1565 child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
1566 } else if (child == end_item) {
1567 // eg "ABCdef" -> <span>"ABC"</span>"def"
1568 // (includes case where start_text_iter == begin())
1569 // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later
1570 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1571
1572 common_ancestor->getRepr()->addChild(child_span, prev_repr);
1573 Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(0, end_char_index).c_str());
1574 child_span->appendChild(text_in_span);
1575 Inkscape::GC::release(text_in_span);
1576 child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
1577 } else if (start_text_iter != string_item->string.begin()) {
1578 // eg "abcDEF" -> "abc"<span>"DEF"</span>
1579 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1580
1581 Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
1582 common_ancestor->getRepr()->addChild(text_before, prev_repr);
1583 common_ancestor->getRepr()->addChild(child_span, text_before);
1584 Inkscape::GC::release(text_before);
1585 Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index).c_str());
1586 child_span->appendChild(text_in_span);
1587 Inkscape::GC::release(text_in_span);
1588 child->deleteObject();
1589 child = common_ancestor->get_child_by_repr(child_span);
1590
1591 } else
1592 surround_entire_string = true;
1593 }
1594 if (surround_entire_string) {
1595 Inkscape::XML::Node *child_repr = child->getRepr();
1596 common_ancestor->getRepr()->addChild(child_span, child_repr);
1597 Inkscape::GC::anchor(child_repr);
1598 common_ancestor->getRepr()->removeChild(child_repr);
1599 child_span->appendChild(child_repr);
1600 Inkscape::GC::release(child_repr);
1601 child = common_ancestor->get_child_by_repr(child_span);
1602 }
1603 Inkscape::GC::release(child_span);
1604
1605 } else if (child != end_item) { // not a string and we're applying to the entire object. This is easy
1606 apply_css_recursive(child, css);
1607 }
1608
1609 } else { // !passed_start
1610 if (child->isAncestorOf(start_item)) {
1611 recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name);
1612 if (end_item && child->isAncestorOf(end_item))
1613 break; // only happens when start_item == end_item (I think)
1614 passed_start = true;
1615 }
1616 }
1617
1618 if (end_item == child)
1619 break;
1620 }
1621 }
1622
1623 /* if item is at the beginning of a tree it doesn't matter which element
1624 it points to so for neatness we would like it to point to the highest
1625 possible child of \a common_ancestor. There is no iterator return because
1626 a string can never be an ancestor.
1627
1628 eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would
1629 like * to point to the inner span because we can apply style to that whole
1630 span. */
ascend_while_first(SPObject * item,Glib::ustring::iterator text_iter,SPObject * common_ancestor)1631 static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor)
1632 {
1633 if (item == common_ancestor)
1634 return item;
1635 if (SP_IS_STRING(item))
1636 if (text_iter != SP_STRING(item)->string.begin())
1637 return item;
1638 for ( ; ; ) {
1639 SPObject *parent = item->parent;
1640 if (parent == common_ancestor) {
1641 break;
1642 }
1643 if (item != parent->firstChild()) {
1644 break;
1645 }
1646 item = parent;
1647 }
1648 return item;
1649 }
1650
1651
1652 /** empty spans: abc<span></span>def
1653 -> abcdef */
tidy_operator_empty_spans(SPObject ** item,bool)1654 static bool tidy_operator_empty_spans(SPObject **item, bool /*has_text_decoration*/)
1655 {
1656 bool result = false;
1657 if ( !(*item)->hasChildren()
1658 && !is_line_break_object(*item)
1659 && !(SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty())
1660 ) {
1661 SPObject *next = (*item)->getNext();
1662 (*item)->deleteObject();
1663 *item = next;
1664 result = true;
1665 }
1666 return result;
1667 }
1668
1669 /** inexplicable spans: abc<span style="">def</span>ghi
1670 -> "abc""def""ghi"
1671 the repeated strings will be merged by another operator. */
tidy_operator_inexplicable_spans(SPObject ** item,bool)1672 static bool tidy_operator_inexplicable_spans(SPObject **item, bool /*has_text_decoration*/)
1673 {
1674 //XML Tree being directly used here while it shouldn't be.
1675 if (*item && sp_repr_is_meta_element((*item)->getRepr())) {
1676 return false;
1677 }
1678 if (SP_IS_STRING(*item)) {
1679 return false;
1680 }
1681 if (is_line_break_object(*item)) {
1682 return false;
1683 }
1684 TextTagAttributes *attrs = attributes_for_object(*item);
1685 if (attrs && attrs->anyAttributesSet()) {
1686 return false;
1687 }
1688 if (!objects_have_equal_style((*item)->parent, *item)) {
1689 return false;
1690 }
1691 SPObject *next = *item;
1692 while ((*item)->hasChildren()) {
1693 Inkscape::XML::Node *repr = (*item)->firstChild()->getRepr();
1694 Inkscape::GC::anchor(repr);
1695 (*item)->getRepr()->removeChild(repr);
1696 (*item)->parent->getRepr()->addChild(repr, next->getRepr());
1697 Inkscape::GC::release(repr);
1698 next = next->getNext();
1699 }
1700 (*item)->deleteObject();
1701 *item = next;
1702 return true;
1703 }
1704
1705 /** repeated spans: <font a>abc</font><font a>def</font>
1706 -> <font a>abcdef</font> */
tidy_operator_repeated_spans(SPObject ** item,bool)1707 static bool tidy_operator_repeated_spans(SPObject **item, bool /*has_text_decoration*/)
1708 {
1709 SPObject *first = *item;
1710 SPObject *second = first->getNext();
1711 if (second == nullptr) return false;
1712
1713 Inkscape::XML::Node *first_repr = first->getRepr();
1714 Inkscape::XML::Node *second_repr = second->getRepr();
1715
1716 if (first_repr->type() != second_repr->type()) return false;
1717
1718 if (SP_IS_STRING(first) && SP_IS_STRING(second)) {
1719 // also amalgamate consecutive SPStrings into one
1720 Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string;
1721 first->getRepr()->setContent(merged_string.c_str());
1722 second_repr->parent()->removeChild(second_repr);
1723 return true;
1724 }
1725
1726 // merge consecutive spans with identical styles into one
1727 if (first_repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) return false;
1728 if (strcmp(first_repr->name(), second_repr->name()) != 0) return false;
1729 if (is_line_break_object(second)) return false;
1730 gchar const *first_style = first_repr->attribute("style");
1731 gchar const *second_style = second_repr->attribute("style");
1732 if (!((first_style == nullptr && second_style == nullptr)
1733 || (first_style != nullptr && second_style != nullptr && !strcmp(first_style, second_style))))
1734 return false;
1735
1736 // all our tests passed: do the merge
1737 TextTagAttributes *attributes_first = attributes_for_object(first);
1738 TextTagAttributes *attributes_second = attributes_for_object(second);
1739 if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) {
1740 TextTagAttributes attributes_first_copy = *attributes_first;
1741 attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first));
1742 }
1743 move_child_nodes(second_repr, first_repr);
1744 second_repr->parent()->removeChild(second_repr);
1745 return true;
1746 // *item is still the next object to process
1747 }
1748
1749 /** redundant nesting: <font a><font b>abc</font></font>
1750 -> <font b>abc</font>
1751 excessive nesting: <font a><size 1>abc</size></font>
1752 -> <font a,size 1>abc</font> */
tidy_operator_excessive_nesting(SPObject ** item,bool)1753 static bool tidy_operator_excessive_nesting(SPObject **item, bool /*has_text_decoration*/)
1754 {
1755 if (!(*item)->hasChildren()) {
1756 return false;
1757 }
1758 if ((*item)->firstChild() != (*item)->lastChild()) {
1759 return false;
1760 }
1761 if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild())) {
1762 return false;
1763 }
1764 if (SP_IS_STRING((*item)->firstChild())) {
1765 return false;
1766 }
1767 if (is_line_break_object((*item)->firstChild())) {
1768 return false;
1769 }
1770 TextTagAttributes *attrs = attributes_for_object((*item)->firstChild());
1771 if (attrs && attrs->anyAttributesSet()) {
1772 return false;
1773 }
1774 gchar const *child_style = (*item)->firstChild()->getRepr()->attribute("style");
1775 if (child_style && *child_style) {
1776 overwrite_style_with_string(*item, child_style);
1777 }
1778 move_child_nodes((*item)->firstChild()->getRepr(), (*item)->getRepr());
1779 (*item)->firstChild()->deleteObject();
1780 return true;
1781 }
1782
1783 /** helper for tidy_operator_redundant_double_nesting() */
redundant_double_nesting_processor(SPObject ** item,SPObject * child,bool prepend)1784 static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1785 {
1786 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) {
1787 return false;
1788 }
1789 if (SP_IS_STRING(child)) {
1790 return false;
1791 }
1792 if (is_line_break_object(child)) {
1793 return false;
1794 }
1795 if (is_line_break_object(*item)) {
1796 return false;
1797 }
1798 TextTagAttributes *attrs = attributes_for_object(child);
1799 if (attrs && attrs->anyAttributesSet()) {
1800 return false;
1801 }
1802 if (!objects_have_equal_style((*item)->parent, child)) {
1803 return false;
1804 }
1805
1806 Inkscape::XML::Node *insert_after_repr = nullptr;
1807 if (!prepend) {
1808 insert_after_repr = (*item)->getRepr();
1809 } else if ((*item)->getPrev()) {
1810 insert_after_repr = (*item)->getPrev()->getRepr();
1811 }
1812 while (child->getRepr()->childCount()) {
1813 Inkscape::XML::Node *move_repr = child->getRepr()->firstChild();
1814 Inkscape::GC::anchor(move_repr);
1815 child->getRepr()->removeChild(move_repr);
1816 (*item)->parent->getRepr()->addChild(move_repr, insert_after_repr);
1817 Inkscape::GC::release(move_repr);
1818 insert_after_repr = move_repr; // I think this will stay valid long enough. It's garbage collected these days.
1819 }
1820 child->deleteObject();
1821 return true;
1822 }
1823
1824 /** redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font>
1825 -> <font b>abc<font a>def</font>ghi</font>
1826 this function does its work when the parameter is the <font a> tag in the
1827 example. You may note that this only does its work when the doubly-nested
1828 child is the first or last. The other cases are called 'style inversion'
1829 below, and I'm not yet convinced that the result of that operation will be
1830 tidier in all cases. */
tidy_operator_redundant_double_nesting(SPObject ** item,bool)1831 static bool tidy_operator_redundant_double_nesting(SPObject **item, bool /*has_text_decoration*/)
1832 {
1833 if (!(*item)->hasChildren()) return false;
1834 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is excessive nesting, done above
1835 if (redundant_double_nesting_processor(item, (*item)->firstChild(), true))
1836 return true;
1837 if (redundant_double_nesting_processor(item, (*item)->lastChild(), false))
1838 return true;
1839 return false;
1840 }
1841
1842 /** helper for tidy_operator_redundant_semi_nesting(). Checks a few things,
1843 then compares the styles for item+child versus just child. If they're equal,
1844 tidying is possible. */
redundant_semi_nesting_processor(SPObject ** item,SPObject * child,bool prepend)1845 static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1846 {
1847 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1848 return false;
1849 if (SP_IS_STRING(child)) return false;
1850 if (is_line_break_object(child)) return false;
1851 if (is_line_break_object(*item)) return false;
1852 TextTagAttributes *attrs = attributes_for_object(child);
1853 if (attrs && attrs->anyAttributesSet()) return false;
1854 attrs = attributes_for_object(*item);
1855 if (attrs && attrs->anyAttributesSet()) return false;
1856
1857 SPCSSAttr *css_child_and_item = sp_repr_css_attr_new();
1858 SPCSSAttr *css_child_only = sp_repr_css_attr_new();
1859 gchar const *item_style = (*item)->getRepr()->attribute("style");
1860 if (item_style && *item_style) {
1861 sp_repr_css_attr_add_from_string(css_child_and_item, item_style);
1862 }
1863 gchar const *child_style = child->getRepr()->attribute("style");
1864 if (child_style && *child_style) {
1865 sp_repr_css_attr_add_from_string(css_child_and_item, child_style);
1866 sp_repr_css_attr_add_from_string(css_child_only, child_style);
1867 }
1868 bool equal = css_attrs_are_equal(css_child_only, css_child_and_item);
1869 sp_repr_css_attr_unref(css_child_and_item);
1870 sp_repr_css_attr_unref(css_child_only);
1871 if (!equal) return false;
1872
1873 Inkscape::XML::Document *xml_doc = (*item)->getRepr()->document();
1874 Inkscape::XML::Node *new_span = xml_doc->createElement((*item)->getRepr()->name());
1875 if (prepend) {
1876 SPObject *prev = (*item)->getPrev();
1877 (*item)->parent->getRepr()->addChild(new_span, prev ? prev->getRepr() : nullptr);
1878 } else {
1879 (*item)->parent->getRepr()->addChild(new_span, (*item)->getRepr());
1880 }
1881 new_span->setAttribute("style", child->getRepr()->attribute("style"));
1882 move_child_nodes(child->getRepr(), new_span);
1883 Inkscape::GC::release(new_span);
1884 child->deleteObject();
1885 return true;
1886 }
1887
1888 /** redundant semi-nesting: <font a><font b>abc</font>def</font>
1889 -> <font b>abc</font><font>def</font>
1890 test this by applying a colour to a region, then a different colour to
1891 a partially-overlapping region. */
tidy_operator_redundant_semi_nesting(SPObject ** item,bool)1892 static bool tidy_operator_redundant_semi_nesting(SPObject **item, bool /*has_text_decoration*/)
1893 {
1894 if (!(*item)->hasChildren()) return false;
1895 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is redundant nesting, done above
1896 if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true))
1897 return true;
1898 if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false))
1899 return true;
1900 return false;
1901 }
1902
1903
1904 /* tidy_operator_styled_whitespace commented out: not only did it have bugs,
1905 * but it did *not* preserve the rendering: spaces in different font sizes,
1906 * for instance, have different width, so moving them out of tspans changes
1907 * the document. cf https://bugs.launchpad.net/inkscape/+bug/1477723
1908 */
1909
1910 #if 0
1911 /** helper for tidy_operator_styled_whitespace(), finds the last string object
1912 in a paragraph which is not \a not_obj. */
1913 static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj)
1914 {
1915 for (SPObject *child = root->lastChild() ; child ; child = child->getPrev())
1916 {
1917 if (child == not_obj) continue;
1918 if (child->hasChildren()) {
1919 SPString *ret = find_last_string_child_not_equal_to(child, not_obj);
1920 if (ret) return ret;
1921 } else if (SP_IS_STRING(child))
1922 return SP_STRING(child);
1923 }
1924 return NULL;
1925 }
1926
1927 /** whitespace-only spans:
1928 abc<font> </font>def
1929 -> abc<font></font> def
1930 abc<b><i>def</i> </b>ghi
1931 -> abc<b><i>def</i></b> ghi
1932
1933 visible text-decoration changes on white spaces should not be subject
1934 to this sort of processing. So
1935 abc<text-decoration-color> </text-decoration-color>def
1936 is unchanged.
1937 */
1938 static bool tidy_operator_styled_whitespace(SPObject **item, bool has_text_decoration)
1939 {
1940 // any type of visible text decoration is OK as pure spaces, so do nothing here in that case.
1941 if (!SP_IS_STRING(*item) || has_text_decoration ) {
1942 return false;
1943 }
1944 Glib::ustring const &str = SP_STRING(*item)->string;
1945 for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it) {
1946 if (!g_unichar_isspace(*it)) {
1947 return false;
1948 }
1949 }
1950
1951
1952 SPObject *test_item = *item;
1953 SPString *next_string;
1954 for ( ; ; ) { // find the next string
1955 next_string = sp_te_seek_next_string_recursive(test_item->getNext());
1956 if (next_string) {
1957 next_string->string.insert(0, str);
1958 break;
1959 }
1960 for ( ; ; ) { // go up one item in the xml
1961 test_item = test_item->parent;
1962 if (is_line_break_object(test_item)) {
1963 break;
1964 }
1965 if (SP_IS_FLOWTEXT(test_item)) {
1966 return false;
1967 }
1968 SPObject *next = test_item->getNext();
1969 if (next) {
1970 test_item = next;
1971 break;
1972 }
1973 }
1974 if (is_line_break_object(test_item)) { // no next string, see if there's a prev string
1975 next_string = find_last_string_child_not_equal_to(test_item, *item);
1976 if (next_string == NULL) {
1977 return false; // an empty paragraph
1978 }
1979 next_string->string = str + next_string->string;
1980 break;
1981 }
1982 }
1983 next_string->getRepr()->setContent(next_string->string.c_str());
1984 SPObject *delete_obj = *item;
1985 *item = (*item)->getNext();
1986 delete_obj->deleteObject();
1987 return true;
1988 }
1989 #endif
1990
1991 /* possible tidy operators that are not yet implemented, either because
1992 they are difficult, occur infrequently, or because I'm not sure that the
1993 output is tidier in all cases:
1994 duplicate styles in line break elements: <div italic><para italic>abc</para></div>
1995 -> <div italic><para>abc</para></div>
1996 style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font>
1997 -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font>
1998 mistaken precedence: <font a,size 1>abc</font><size 1>def</size>
1999 -> <size 1><font a>abc</font>def</size>
2000 */
2001
2002 /** Recursively walks the xml tree calling a set of cleanup operations on
2003 every child. Returns true if any changes were made to the tree.
2004
2005 All the tidy operators return true if they made changes, and alter their
2006 parameter to point to the next object that should be processed, or NULL.
2007 They must not significantly alter (ie delete) any ancestor elements of the
2008 one they are passed.
2009
2010 It may be that some of the later tidy operators that I wrote are actually
2011 general cases of the earlier operators, and hence the special-case-only
2012 versions can be removed. I haven't analysed my work in detail to figure
2013 out if this is so. */
tidy_xml_tree_recursively(SPObject * root,bool has_text_decoration)2014 static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration)
2015 {
2016 gchar const *root_style = (root)->getRepr()->attribute("style");
2017 if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
2018 static bool (* const tidy_operators[])(SPObject**, bool) = {
2019 tidy_operator_empty_spans,
2020 tidy_operator_inexplicable_spans,
2021 tidy_operator_repeated_spans,
2022 tidy_operator_excessive_nesting,
2023 tidy_operator_redundant_double_nesting,
2024 tidy_operator_redundant_semi_nesting
2025 };
2026 bool changes = false;
2027
2028 for (SPObject *child = root->firstChild() ; child != nullptr ; ) {
2029 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child) || SP_IS_TREF(child)) {
2030 child = child->getNext();
2031 continue;
2032 }
2033 if (child->hasChildren()) {
2034 changes |= tidy_xml_tree_recursively(child, has_text_decoration);
2035 }
2036
2037 unsigned i;
2038 for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) {
2039 if (tidy_operators[i](&child, has_text_decoration)) {
2040 changes = true;
2041 break;
2042 }
2043 }
2044 if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0])) {
2045 child = child->getNext();
2046 }
2047 }
2048 return changes;
2049 }
2050
2051 /** Applies the given CSS fragment to the characters of the given text or
2052 flowtext object between \a start and \a end, creating or removing span
2053 elements as necessary and optimal. */
sp_te_apply_style(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPCSSAttr const * css)2054 void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css)
2055 {
2056 // in the comments in the code below, capital letters are inside the application region, lowercase are outside
2057 if (start == end) return;
2058 Inkscape::Text::Layout::iterator first, last;
2059 if (start < end) {
2060 first = start;
2061 last = end;
2062 } else {
2063 first = end;
2064 last = start;
2065 }
2066 Inkscape::Text::Layout const *layout = te_get_layout(text);
2067 SPObject *start_item = nullptr, *end_item = nullptr;
2068 Glib::ustring::iterator start_text_iter, end_text_iter;
2069 layout->getSourceOfCharacter(first, &start_item, &start_text_iter);
2070 layout->getSourceOfCharacter(last, &end_item, &end_text_iter);
2071 if (start_item == nullptr) {
2072 return; // start is at end of text
2073 }
2074 if (is_line_break_object(start_item)) {
2075 start_item = start_item->getNext();
2076 }
2077 if (is_line_break_object(end_item)) {
2078 end_item = end_item->getNext();
2079 }
2080 if (end_item == nullptr) {
2081 end_item = text;
2082 }
2083
2084
2085 /* Special case: With a tref, we only want to change its style when the whole
2086 * string is selected, in which case the style can be applied directly to the
2087 * tref node. If only part of the tref's string child is selected, just return. */
2088
2089 if (!sp_tref_fully_contained(start_item, start_text_iter, end_item, end_text_iter)) {
2090
2091 return;
2092 }
2093
2094 /* stage 1: applying the style. Go up to the closest common ancestor of
2095 start and end and then semi-recursively apply the style to all the
2096 objects in between. The semi-recursion is because it's only necessary
2097 at the beginning and end; the style can just be applied to the root
2098 child in the middle.
2099 eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span>
2100 The recursion may involve creating new spans.
2101 */
2102 SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item);
2103
2104 // bug #168370 (consider parent transform and viewBox)
2105 // snipplet copied from desktop-style.cpp sp_desktop_apply_css_recursive(...)
2106 SPCSSAttr *css_set = sp_repr_css_attr_new();
2107 sp_repr_css_merge(css_set, const_cast<SPCSSAttr*>(css));
2108 {
2109 Geom::Affine const local(SP_ITEM(common_ancestor)->i2doc_affine());
2110 double const ex(local.descrim());
2111 if ( ( ex != 0. )
2112 && ( ex != 1. ) ) {
2113 sp_css_attr_scale(css_set, 1/ex);
2114 }
2115 }
2116
2117 start_item = ascend_while_first(start_item, start_text_iter, common_ancestor);
2118 end_item = ascend_while_first(end_item, end_text_iter, common_ancestor);
2119 recursively_apply_style(common_ancestor, css_set, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text));
2120 sp_repr_css_attr_unref(css_set);
2121
2122 /* stage 2: cleanup the xml tree (of which there are multiple passes) */
2123 /* discussion: this stage requires a certain level of inventiveness because
2124 it's not clear what the best representation is in many cases. An ideal
2125 implementation would provide some sort of scoring function to rate the
2126 ugliness of a given xml tree and try to reduce said function, but providing
2127 the various possibilities to be rated is non-trivial. Instead, I have opted
2128 for a multi-pass technique which simply recognises known-ugly patterns and
2129 has matching routines for optimising the patterns it finds. It's reasonably
2130 easy to add new pattern matching processors. If everything gets disastrous
2131 and neither option can be made to work, a fallback could be to reduce
2132 everything to a single level of nesting and drop all pretense of
2133 roundtrippability. */
2134 bool has_text_decoration = false;
2135 gchar const *root_style = (text)->getRepr()->attribute("style");
2136 if(root_style && strstr(root_style,"text-decoration")) has_text_decoration = true;
2137 while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
2138
2139 // if we only modified subobjects this won't have been automatically sent
2140 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
2141 }
2142
is_part_of_text_subtree(SPObject * obj)2143 bool is_part_of_text_subtree (SPObject *obj)
2144 {
2145 return (SP_IS_TSPAN(obj)
2146 || SP_IS_TEXT(obj)
2147 || SP_IS_FLOWTEXT(obj)
2148 || SP_IS_FLOWTSPAN(obj)
2149 || SP_IS_FLOWDIV(obj)
2150 || SP_IS_FLOWPARA(obj)
2151 || SP_IS_FLOWLINE(obj)
2152 || SP_IS_FLOWREGIONBREAK(obj));
2153 }
2154
is_top_level_text_object(SPObject * obj)2155 bool is_top_level_text_object (SPObject *obj)
2156 {
2157 return (SP_IS_TEXT(obj)
2158 || SP_IS_FLOWTEXT(obj));
2159 }
2160
has_visible_text(SPObject * obj)2161 bool has_visible_text(SPObject *obj)
2162 {
2163 bool hasVisible = false;
2164
2165 if (SP_IS_STRING(obj) && !SP_STRING(obj)->string.empty()) {
2166 hasVisible = true; // maybe we should also check that it's not all whitespace?
2167 } else {
2168 for (auto& child: obj->children) {
2169 if (has_visible_text(const_cast<SPObject *>(&child))) {
2170 hasVisible = true;
2171 break;
2172 }
2173 }
2174 }
2175
2176 return hasVisible;
2177 }
2178
2179 /*
2180 Local Variables:
2181 mode:c++
2182 c-file-style:"stroustrup"
2183 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2184 indent-tabs-mode:nil
2185 fill-column:99
2186 End:
2187 */
2188 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
2189