Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextShape.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
3 * SPDX-FileCopyrightText: 2022 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include "KoSvgTextShape.h"
9#include "KoSvgTextShape_p.h"
10
11#include <QTextLayout>
12
13#include <klocalizedstring.h>
14
15#include "KoSvgTextProperties.h"
17#include <KoShapeContainer_p.h>
18#include <KoShapeController.h>
19#include <text/KoCssTextUtils.h>
20#include <text/KoFontRegistry.h>
22#include <text/KoPolygonUtils.h>
23
24#include <kis_global.h>
25
26#include <KoClipMaskPainter.h>
27#include <KoColorBackground.h>
28#include <KoIcon.h>
29#include <KoPathShape.h>
30#include <KoProperties.h>
32#include <KoXmlNS.h>
33#include <KoInsets.h>
34
35#include <SvgLoadingContext.h>
36#include <SvgGraphicContext.h>
37#include <SvgUtil.h>
38#include <SvgStyleWriter.h>
39
40#include <QPainter>
41#include <QPainterPath>
42#include <QtMath>
43
44#include <FlakeDebug.h>
45#include <functional>
46
47// Memento pointer to hold data for Undo commands.
48
88
91 :textElement(_textElement)
92 , textPath(_textPath){}
94 // textPath is owned by its textshape.
95 KoShape *textPath = nullptr;
96};
97
98// for the use in KoSvgTextShape::Private::createTextNodeIndex() only
100 : d() // Private is **not** initialized, to be initialized by the factory method
101{
102}
103
104KoSvgTextNodeIndex KoSvgTextShape::Private::createTextNodeIndex(KisForest<KoSvgTextContentElement>::child_iterator textElement) const
105{
106 KoSvgTextNodeIndex index;
107 index.d.reset(new KoSvgTextNodeIndex::Private(textElement, Private::textPathByName(textElement->textPathId, textPaths)));
108 return index;
109}
110
112{
113 d.reset(new KoSvgTextNodeIndex::Private(rhs.d->textElement, rhs.d->textPath));
114}
115
119
121 if (!d) { return nullptr;}
122 return &d->textElement->properties;
123}
124
126 if (!d) {
127 qDebug() << "d not initialized...";
128 return nullptr;
129 }
130 return &d->textElement->textPathInfo;
131}
132
134 if (!d) { qDebug() << "d not initialized...";return nullptr;}
135 return d->textPath;
136}
137
139 : KoShape()
140 , d(new Private)
141{
143 d->shapeGroup->setTransformation(this->transformation());
144 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
145 d->internalShapesPainter->setUpdateFunction(std::bind(&KoSvgTextShape::updateAbsolute, this, std::placeholders::_1));
146}
147
149 : KoShape(rhs)
150 , d(new Private(*rhs.d))
151{
153 d->shapeGroup->setTransformation(this->transformation());
154 d->internalShapesPainter->setUpdateFunction(std::bind(&KoSvgTextShape::updateAbsolute, this, std::placeholders::_1));
155 Q_FOREACH(KoShape *shape, d->shapeGroup->shapes()) {
156 shape->addDependee(this);
157 }
158}
159
161{
162 Q_FOREACH(KoShape *shape, internalShapeManager()->shapes()) {
163 shape->removeDependee(this);
164 }
165}
166
168{
169 static const QString s_placeholderText = i18nc("Default text for the text shape", "Placeholder Text");
170 return s_placeholderText;
171}
172
174{
175 return new KoSvgTextShape(*this);
176}
177
179{
180 KoShape::shapeChanged(type, shape);
181
182 if (d->isLoading) {
183 return;
184 }
185
187
188 if ((d->shapesInside.contains(shape) || d->shapesSubtract.contains(shape) || d->textPaths.contains(shape))
189 && (transformationTypes.contains(type)
190 || type == ParameterChanged
191 || type == ParentChanged
192 || type == ContentChanged // for KoPathShape modifications
193 || type == Deleted)) {
194 if (type == Deleted) {
195 if (d->shapesInside.contains(shape)) {
196 d->shapesInside.removeAll(shape);
197 }
198 if (d->shapesSubtract.contains(shape)) {
199 d->shapesSubtract.removeAll(shape);
200 }
201 if (d->textPaths.contains(shape)) {
202 d->textPaths.removeAll(shape);
203 // TODO: remove ID from relevant text content element.
204 }
205 if (d->shapeGroup && d->shapeGroup->shapes().contains(shape)) {
206 d->shapeGroup->removeShape(shape);
207 }
208 d->updateInternalShapesList();
209 }
210
211 // Updates the contours and calls relayout.
212 // Would be great if we could compress the updates here somehow...
213 if (d->bulkActionState) {
214 d->bulkActionState->contourHasChanged = true;
215 } else {
216 d->updateTextWrappingAreas();
217 }
218
219 // NotifyChanged ensures that boundingRect() is called on this shape;
220 // it is NOT compressed by the bulk action, since boundingRect() is
221 // guaranteed to be valid during the whole bulk action
222 this->notifyChanged();
223 }
224 if ((!shape || shape == this)) {
225 if (type == ContentChanged) {
226 if (d->bulkActionState) {
227 d->bulkActionState->layoutHasChanged = true;
228 } else {
229 relayout();
230 }
231 } else if (transformationTypes.contains(type) || type == ParentChanged) {
232 d->shapeGroup->setTransformation(this->absoluteTransformation());
233 } else if (type == TextContourMarginChanged) {
234 if (d->bulkActionState) {
235 d->bulkActionState->contourHasChanged = true;
236 } else {
237 d->updateTextWrappingAreas();
238 }
239 }
240
241 // when calling shapeChangedImpl() on `this` shape, we call
242 // notifyChanged() manually at the calling site, so we shouldn't
243 // do that again here
244 }
245}
246
247void KoSvgTextShape::setResolution(qreal xRes, qreal yRes)
248{
249 int roundedX = qRound(xRes);
250 int roundedY = qRound(yRes);
251 if (roundedX != d->xRes || roundedY != d->yRes) {
252 d->xRes = roundedX;
253 d->yRes = roundedY;
254 relayout();
255 }
256}
257
258int KoSvgTextShape::posLeft(int pos, bool visual)
259{
262 if (mode == KoSvgText::VerticalRL) {
263 return nextLine(pos);
264 } else if (mode == KoSvgText::VerticalLR) {
265 return previousLine(pos);
266 } else {
267 if (direction == KoSvgText::DirectionRightToLeft) {
268 return nextPos(pos, visual);
269 } else {
270 return previousPos(pos, visual);
271 }
272 }
273}
274
275int KoSvgTextShape::posRight(int pos, bool visual)
276{
279
280 if (mode == KoSvgText::VerticalRL) {
281 return previousLine(pos);
282 } else if (mode == KoSvgText::VerticalLR) {
283 return nextLine(pos);
284 } else {
285 if (direction == KoSvgText::DirectionRightToLeft) {
286 return previousPos(pos, visual);
287 } else {
288 return nextPos(pos, visual);
289 }
290 }
291}
292
293int KoSvgTextShape::posUp(int pos, bool visual)
294{
297 if (mode == KoSvgText::VerticalRL || mode == KoSvgText::VerticalLR) {
298 if (direction == KoSvgText::DirectionRightToLeft) {
299 return nextPos(pos, visual);
300 } else {
301 return previousPos(pos, visual);
302 }
303 } else {
304 return previousLine(pos);
305 }
306}
307
308int KoSvgTextShape::posDown(int pos, bool visual)
309{
312 if (mode == KoSvgText::VerticalRL || mode == KoSvgText::VerticalLR) {
313 if (direction == KoSvgText::DirectionRightToLeft) {
314 return previousPos(pos, visual);
315 } else {
316 return nextPos(pos, visual);
317 }
318 } else {
319 return nextLine(pos);
320 }
321}
322
324{
325 if (pos < 0 || d->cursorPos.isEmpty() || d->result.isEmpty()) {
326 return pos;
327 }
328 CursorPos p = d->cursorPos.at(pos);
329 if (d->result.at(p.cluster).anchored_chunk && p.offset == 0) {
330 return pos;
331 }
332 int candidate = 0;
333 for (int i = 0; i < pos; i++) {
334 CursorPos p2 = d->cursorPos.at(i);
335 if (d->result.at(p2.cluster).anchored_chunk && p2.offset == 0) {
336 candidate = i;
337 }
338 }
339 return candidate;
340}
341
343{
344 if (pos < 0 || d->cursorPos.isEmpty() || d->result.isEmpty()) {
345 return pos;
346 }
347 if (pos > d->cursorPos.size() - 1) {
348 return d->cursorPos.size() - 1;
349 }
350 int candidate = 0;
351 int posCluster = d->cursorPos.at(pos).cluster;
352 for (int i = pos; i < d->cursorPos.size(); i++) {
353 CursorPos p = d->cursorPos.at(i);
354 if (d->result.at(p.cluster).anchored_chunk && i > pos && posCluster != p.cluster) {
355 break;
356 }
357 candidate = i;
358 }
359 return candidate;
360}
361
362int KoSvgTextShape::wordLeft(int pos, bool visual)
363{
364 //TODO: figure out preferred behaviour for wordLeft in RTL && visual.
365 Q_UNUSED(visual)
366 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
367 return pos;
368 }
370 if (direction == KoSvgText::DirectionRightToLeft) {
371 return wordEnd(pos);
372 }
373 return wordStart(pos);
374}
375
376int KoSvgTextShape::wordRight(int pos, bool visual)
377{
378 Q_UNUSED(visual)
379 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
380 return pos;
381 }
383 if (direction == KoSvgText::DirectionRightToLeft) {
384 return wordStart(pos);
385 }
386 return wordEnd(pos);
387}
388
390{
391 if (d->cursorPos.isEmpty()) {
392 return pos;
393 }
394 int currentIndex = d->cursorPos.at(pos).index;
395
396 for (int i = pos; i < d->cursorPos.size(); i++) {
397 if (d->cursorPos.at(i).index > currentIndex) {
398 return i;
399 }
400 }
401 return pos;
402}
403
405{
406 if (d->cursorPos.isEmpty()) {
407 return pos;
408 }
409 int currentIndex = d->cursorPos.at(pos).index;
410
411 for (int i = pos; i >= 0; i--) {
412 if (d->cursorPos.at(i).index < currentIndex) {
413 return i;
414 }
415 }
416 return pos;
417}
418
419int KoSvgTextShape::nextPos(int pos, bool visual)
420{
421 if (d->cursorPos.isEmpty()) {
422 return -1;
423 }
424
425 if(visual) {
426 int visualIndex = d->logicalToVisualCursorPos.value(pos);
427 return d->logicalToVisualCursorPos.key(qMin(visualIndex + 1, d->cursorPos.size() - 1), d->cursorPos.size() - 1);
428 }
429
430 return qMin(pos + 1, d->cursorPos.size() - 1);
431}
432
433int KoSvgTextShape::previousPos(int pos, bool visual)
434{
435 if (d->cursorPos.isEmpty()) {
436 return -1;
437 }
438
439 if(visual) {
440 int visualIndex = d->logicalToVisualCursorPos.value(pos);
441 return d->logicalToVisualCursorPos.key(qMax(visualIndex - 1, 0), 0);
442 }
443
444 return qMax(pos - 1, 0);
445}
446
448{
449 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
450 return pos;
451 }
452
453 int nextLineStart = lineEnd(pos)+1;
454 int nextLineEnd = lineEnd(nextLineStart);
455 CursorPos cursorPos = d->cursorPos.at(pos);
456 if (!this->shapesInside().isEmpty()) {
457 LineBox nextLineBox;
458 for (int i = 0; i < d->lineBoxes.size(); ++i) {
459 for (int j = 0; j < d->lineBoxes.at(i).chunks.size(); ++j) {
460 if (d->lineBoxes.at(i).chunks.at(j).chunkIndices.contains(cursorPos.cluster)) {
461 nextLineBox = d->lineBoxes.at(qMin(i + 1, d->lineBoxes.size()-1));
462 }
463 }
464 }
465 if (nextLineBox.chunks.size()>0) {
466 int first = -1;
467 int last = -1;
468 Q_FOREACH(LineChunk chunk, nextLineBox.chunks) {
469 Q_FOREACH (int i, chunk.chunkIndices) {
470 if (d->result.at(i).addressable) {
471 if (first < 0) {
472 first = d->result.at(i).cursorInfo.graphemeIndices.first();
473 }
474 last = d->result.at(i).cursorInfo.graphemeIndices.last();
475 }
476 }
477 }
478 if (first > -1 && last > -1) {
479 nextLineStart = posForIndex(first);
480 nextLineEnd = posForIndex(last);
481 }
482 }
483 }
484
485
486 if (nextLineStart > d->cursorPos.size()-1) {
487 return d->cursorPos.size()-1;
488 }
489
490 CharacterResult res = d->result.at(cursorPos.cluster);
491 QPointF currentPoint = res.finalPosition;
492 currentPoint += res.cursorInfo.offsets.value(cursorPos.offset, res.advance);
493 int candidate = posForPoint(currentPoint, nextLineStart, nextLineEnd+1);
494 if (candidate < 0) {
495 return pos;
496 }
497
498 return candidate;
499}
500
502{
503 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
504 return pos;
505 }
506 int currentLineStart = lineStart(pos);
507 if (currentLineStart - 1 < 0) {
508 return 0;
509 }
510 int previousLineStart = lineStart(currentLineStart-1);
511
512 CursorPos cursorPos = d->cursorPos.at(pos);
513 if (!this->shapesInside().isEmpty()) {
514 LineBox previousLineBox;
515 for (int i = 0; i < d->lineBoxes.size(); ++i) {
516 for (int j = 0; j < d->lineBoxes.at(i).chunks.size(); ++j) {
517 if (d->lineBoxes.at(i).chunks.at(j).chunkIndices.contains(cursorPos.cluster)) {
518 previousLineBox = d->lineBoxes.at(qMax(i - 1, 0));
519 }
520 }
521 }
522 if (previousLineBox.chunks.size()>0) {
523 int first = -1;
524 int last = -1;
525 Q_FOREACH(LineChunk chunk, previousLineBox.chunks) {
526 Q_FOREACH (int i, chunk.chunkIndices) {
527 if (d->result.at(i).addressable) {
528 if (first < 0) {
529 first = d->result.at(i).cursorInfo.graphemeIndices.first();
530 }
531 last = d->result.at(i).cursorInfo.graphemeIndices.last();
532 }
533 }
534 }
535 if (first > -1 && last > -1) {
536 previousLineStart = posForIndex(first);
537 currentLineStart = posForIndex(last);
538 }
539 }
540 }
541
542 CharacterResult res = d->result.at(cursorPos.cluster);
543 QPointF currentPoint = res.finalPosition;
544 currentPoint += res.cursorInfo.offsets.value(cursorPos.offset, res.advance);
545 int candidate = posForPoint(currentPoint, previousLineStart, currentLineStart);
546 if (candidate < 0) {
547 return pos;
548 }
549
550 return candidate;
551}
552
554{
555 if (pos < 0 || pos >= d->cursorPos.size() || d->result.isEmpty() || d->cursorPos.isEmpty()) {
556 return pos;
557 }
558
559 int wordEnd = pos;
560 for (int i = pos; i < d->cursorPos.size(); i++) {
561 wordEnd = i;
562 CursorPos cursorPos = d->cursorPos.at(i);
563 bool isWhiteSpace = false;
564 const CharacterResult res = d->result.at(cursorPos.cluster);
565 const int pIndex = res.plaintTextIndex;
566 if (pIndex >= 0 && pIndex < d->plainText.size()) {
567 QChar c = d->plainText.at(pIndex);
568 isWhiteSpace = KoCssTextUtils::IsCssWordSeparator(QString(c)) || c.isSpace();
569 }
571 && (cursorPos.offset == 0 || cursorPos.offset+1 == res.cursorInfo.offsets.size())
572 && i > pos) {
573
574 if (isWhiteSpace) {
575 wordEnd = qMax(pos, wordEnd-1);
576 }
577 break;
578 }
579
580 }
581
582 return wordEnd;
583}
584
586{
587 if (pos <= 0 || pos > d->cursorPos.size() || d->result.isEmpty() || d->cursorPos.isEmpty()) {
588 return pos;
589 }
590
591 int wordStart = pos;
592 for (int i = pos; i >= 0; i--) {
593 wordStart = i;
594 CursorPos cursorPos = d->cursorPos.at(i);
595 const CharacterResult res = d->result.at(cursorPos.cluster);
597 && (cursorPos.offset == 0 || cursorPos.offset+1 == res.cursorInfo.offsets.size())
598 && i < pos) {
599 break;
600 }
601 }
602
603 return wordStart;
604}
605
607{
609 double fontSize = this->textProperties().fontSize().value;
610 QPainterPath p;
611 if (mode == KoSvgText::HorizontalTB) {
612 p.moveTo(0, fontSize*0.2);
613 p.lineTo(0, -fontSize);
614 } else {
615 p.moveTo(-fontSize * 0.5, 0);
616 p.lineTo(fontSize, 0);
617 }
618 p.translate(d->initialTextPosition);
619
620 return p;
621}
622
623QPainterPath KoSvgTextShape::cursorForPos(int pos, QLineF &caret, QColor &color, double bidiFlagSize)
624{
625 if (d->result.isEmpty() || d->cursorPos.isEmpty() || pos < 0 || pos >= d->cursorPos.size()) {
626 return defaultCursorShape();
627 }
628 QPainterPath p;
629
630 CursorPos cursorPos = d->cursorPos.at(pos);
631
632 CharacterResult res = d->result.at(cursorPos.cluster);
633
634 const QTransform tf = res.finalTransform();
635 color = res.cursorInfo.color;
636 caret = res.cursorInfo.caret;
637 caret.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
638
639 p.moveTo(tf.map(caret.p1()));
640 p.lineTo(tf.map(caret.p2()));
641 if (d->isBidi && bidiFlagSize > 0) {
642 int sign = res.cursorInfo.rtl ? -1 : 1;
643 double bidiFlagHalf = bidiFlagSize * 0.5;
644 QPointF point3;
645 QPointF point4;
647 qreal slope = bidiFlagHalf * (caret.dx()/ caret.dy());
648 point3 = QPointF(caret.p2().x() + slope + (sign * bidiFlagHalf), caret.p2().y() + bidiFlagHalf);
649 point4 = QPointF(point3.x() + slope - (sign * bidiFlagHalf),point3.y() + bidiFlagHalf);
650 } else {
651 qreal slope = bidiFlagHalf * (caret.dy()/ caret.dx());
652 point3 = QPointF(caret.p2().x() - bidiFlagHalf, caret.p2().y() - slope + (sign * bidiFlagHalf));
653 point4 = QPointF(point3.x() - bidiFlagHalf, point3.y() - slope - (sign * bidiFlagHalf));
654 }
655 p.lineTo(tf.map(point3));
656 p.lineTo(tf.map(point4));
657 }
658 caret = tf.map(caret);
659
660 return p;
661}
662
663QPainterPath KoSvgTextShape::selectionBoxes(int pos, int anchor)
664{
665 int start = qMin(pos, anchor);
666 int end = qMax(pos, anchor);
667 end = qMin(d->cursorPos.size()-1, end);
668
669 if (start == end || start < 0) {
670 return QPainterPath();
671 }
672
673 QPainterPath p;
674 p.setFillRule(Qt::WindingFill);
675 for (int i = start+1; i <= end; i++) {
676 CursorPos cursorPos = d->cursorPos.at(i);
677 CharacterResult res = d->result.at(cursorPos.cluster);
678 const QTransform tf = res.finalTransform();
679 QLineF first = res.cursorInfo.caret;
680 QLineF last = first;
681 if (res.cursorInfo.rtl) {
682 last.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, res.advance));
683 first.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
684 } else {
685 first.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, QPointF()));
686 last.translate(res.cursorInfo.offsets.value(cursorPos.offset, res.advance));
687 }
688 QPolygonF poly;
689 poly << first.p1() << first.p2() << last.p2() << last.p1() << first.p1();
690 p.addPolygon(tf.map(poly));
691 }
692
693 return p;
694}
695
696QPainterPath KoSvgTextShape::underlines(int pos, int anchor, KoSvgText::TextDecorations decor, KoSvgText::TextDecorationStyle style, qreal minimum, bool thick)
697{
698 int start = qMin(pos, anchor);
699 int end = qMax(pos, anchor);
700
701 if (start == end || start < 0 || end >= d->cursorPos.size()) {
702 return QPainterPath();
703 }
704 QPainterPathStroker stroker;
705
707 stroker.setCapStyle(Qt::FlatCap);
708 if (style == KoSvgText::Solid) {
709 stroker.setDashPattern(Qt::SolidLine);
710 } else if (style == KoSvgText::Dashed) {
711 stroker.setDashPattern(Qt::DashLine);
712 } else if (style == KoSvgText::Dotted) {
713 stroker.setDashPattern(Qt::DotLine);
714 } else {
715 stroker.setDashPattern(Qt::SolidLine);
716 }
717
718 QPainterPath underPath;
719 QPainterPath overPath;
720 QPainterPath middlePath;
721 qint32 strokeWidth = 0;
722 QPointF inset = mode == KoSvgText::HorizontalTB? QPointF(minimum*0.5, 0): QPointF(0, minimum*0.5);
723 for (int i = start+1; i <= end; i++) {
724 CursorPos pos = d->cursorPos.at(i);
725 CharacterResult res = d->result.at(pos.cluster);
726 strokeWidth += res.metrics.underlineThickness;
727 const QTransform tf = res.finalTransform();
728 QPointF first = res.cursorInfo.caret.p1();
729 QPointF last = first;
730 if (res.cursorInfo.rtl) {
731 last += res.cursorInfo.offsets.value(pos.offset-1, res.advance);
732 first += res.cursorInfo.offsets.value(pos.offset, QPointF());
733 if (i == start+1) {
734 first -= inset;
735 }
736 if (i == end) {
737 last += inset;
738 }
739 } else {
740 first += res.cursorInfo.offsets.value(pos.offset-1, QPointF());
741 last += res.cursorInfo.offsets.value(pos.offset, res.advance);
742 if (i == start+1) {
743 first += inset;
744 }
745 if (i == end) {
746 last -= inset;
747 }
748 }
749
750 if (decor.testFlag(KoSvgText::DecorationUnderline)){
751 underPath.moveTo(tf.map(first));
752 underPath.lineTo(tf.map(last));
753 }
754 QPointF diff = res.cursorInfo.caret.p2() - res.cursorInfo.caret.p1();
755 if (decor.testFlag(KoSvgText::DecorationOverline)){
756 overPath.moveTo(tf.map(first+diff));
757 overPath.lineTo(tf.map(last+diff));
758 }
759 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
760 middlePath.moveTo(tf.map(first+(diff*0.5)));
761 middlePath.lineTo(tf.map(last+(diff*0.5)));
762 }
763 }
764
765 const qreal freetypePixelsToPt = (1.0 / 64.0) * (72. / qMin(d->xRes, d->yRes));
766 const qreal width = strokeWidth > 0 ? qMax(qreal(strokeWidth/qMax(1, end-(start+1)))*freetypePixelsToPt, minimum): minimum;
767
768 stroker.setWidth(thick? width*2: width);
769
770 QPainterPath final;
771 if (decor.testFlag(KoSvgText::DecorationUnderline)){
772 final.addPath(stroker.createStroke(underPath));
773 }
774 if (decor.testFlag(KoSvgText::DecorationOverline)){
775 final.addPath(stroker.createStroke(overPath));
776 }
777 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
778 final.addPath(stroker.createStroke(middlePath));
779 }
780
781 return final;
782}
783
784int KoSvgTextShape::posForPoint(QPointF point, int start, int end, bool *overlaps)
785{
786 int a = 0;
787 int b = d->cursorPos.size();
788 if (start >= 0 && end >= 0) {
789 a = qMax(start, a);
790 b = qMin(end, b);
791 }
792 double closest = std::numeric_limits<double>::max();
793 int candidate = 0;
794 for (int i = a; i < b; i++) {
795 CursorPos pos = d->cursorPos.at(i);
796 CharacterResult res = d->result.at(pos.cluster);
797 QPointF cursorStart = res.finalPosition;
798 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
799 double distance = kisDistance(cursorStart, point);
800 if (distance < closest) {
801 candidate = i;
802 closest = distance;
803 if (overlaps) {
804 *overlaps = res.finalTransform().map(res.layoutBox()).containsPoint(point, Qt::WindingFill);
805 }
806 }
807 }
808 return candidate;
809}
810
812{
813 bool overlaps = false;
814 int initialPos = posForPoint(point, -1, -1, &overlaps);
815
816 if (overlaps) {
817 return initialPos;
818 }
819
821
822 int candidateLineStart = 0;
823 double closest = std::numeric_limits<double>::max();
824 for (int i = 0; i < d->cursorPos.size(); i++) {
825 CursorPos pos = d->cursorPos.at(i);
826 CharacterResult res = d->result.at(pos.cluster);
827 if (res.anchored_chunk) {
828 QLineF caret = res.cursorInfo.caret;
829 caret.translate(res.finalPosition);
830 QPointF cursorStart = res.finalPosition;
831 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
832 double distance = kisDistance(cursorStart, point);
833 if (mode == KoSvgText::HorizontalTB) {
834 if (caret.p1().y() > point.y() && caret.p2().y() <= point.y() && closest > distance) {
835 candidateLineStart = i;
836 closest = distance;
837 }
838 } else {
839 if (caret.p2().x() > point.x() && caret.p1().x() <= point.x() && closest > distance) {
840 candidateLineStart = i;
841 closest = distance;
842 }
843 }
844 }
845 }
846
847 if (candidateLineStart > -1) {
848 int end = lineEnd(candidateLineStart);
849 initialPos = posForPoint(point, candidateLineStart, qMin(end + 1, d->cursorPos.size()));
850 }
851
852 return initialPos;
853}
854
855int KoSvgTextShape::posForIndex(int index, bool firstIndex, bool skipSynthetic) const
856{
857 int pos = -1;
858 if (d->cursorPos.isEmpty() || index < 0) {
859 return pos;
860 }
861 for (int i = 0; i< d->cursorPos.size(); i++) {
862 if (skipSynthetic && d->cursorPos.at(i).synthetic) {
863 continue;
864 }
865 if (d->cursorPos.at(i).index <= index) {
866 pos = i;
867 if (d->cursorPos.at(i).index == index && firstIndex) {
868 break;
869 }
870 } else if (d->cursorPos.at(i).index > index) {
871 break;
872 }
873 }
874
875 return pos;
876}
877
879{
880 if (d->cursorPos.isEmpty() || pos < 0) {
881 return -1;
882 }
883
884 return d->cursorPos.at(qMin(d->cursorPos.size()-1, pos)).index;
885}
886
888{
889 return d->initialTextPosition;
890}
891
892bool KoSvgTextShape::insertText(int pos, QString text)
893{
894 bool success = false;
895 int currentIndex = 0;
896
900 int elementIndex = 0;
901 int insertionIndex = 0;
902 if (pos > -1 && !d->cursorPos.isEmpty()) {
903 CursorPos cursorPos = d->cursorPos.at(pos);
904 CharacterResult res = d->result.at(cursorPos.cluster);
905 elementIndex = res.plaintTextIndex;
906 insertionIndex = cursorPos.index;
907 elementIndex = qMin(elementIndex, d->result.size()-1);
908 }
909 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
910 if (it != d->textData.depthFirstTailEnd()) {
911 const int offset = insertionIndex - currentIndex;
912 it->insertText(offset, text);
913
914 d->insertTransforms(d->textData, insertionIndex, text.size(), (elementIndex == insertionIndex));
917 success = true;
918 }
919 return success;
920}
921
922bool KoSvgTextShape::removeText(int &index, int &length)
923{
924 bool success = false;
925 if (index < -1) {
926 return success;
927 }
928 int currentLength = length;
929 int endLength = 0;
930 while (currentLength > 0) {
931 int currentIndex = 0;
932
933 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, index, true);
934 if (it != d->textData.depthFirstTailEnd()) {
935 int offset = index > currentIndex? index - currentIndex: 0;
936 int size = it->numChars(false);
937 it->removeText(offset, currentLength);
938 int diff = size - it->numChars(false);
939 currentLength -= diff;
940 endLength += diff;
941
942 if (index >= currentIndex) {
943 index = currentIndex + offset;
944 }
945
946 d->removeTransforms(d->textData, index, endLength);
947
948 success = true;
949 } else {
950 currentLength = -1;
951 }
952 }
953 if (success) {
954 length = endLength;
957 }
958 return success;
959}
960
961KoSvgTextProperties KoSvgTextShape::propertiesForPos(const int pos, bool inherited) const
962{
963 return propertiesForRange(pos, pos, inherited).value(0, KoSvgTextProperties());
964}
965
968 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it));
969 parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) {
970 hierarchy.append(parentIt);
971 }
973 while (!hierarchy.isEmpty()) {
974 auto it = hierarchy.takeLast();
975 KoSvgTextProperties p = it->properties;
976 p.inheritFrom(props, true);
977 props = p;
978 }
979 return props;
980}
981
982QList<KoSvgTextProperties> KoSvgTextShape::propertiesForRange(const int startPos, const int endPos, bool inherited) const
983{
985
987 if (d->isLoading) return props;
988
989 if (((startPos < 0 || startPos >= d->cursorPos.size()) && startPos == endPos) || d->cursorPos.isEmpty()) {
990 props = {KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties()};
991 return props;
992 }
993 const int finalPos = d->cursorPos.size() - 1;
994 const int startIndex = d->cursorPos.at(qBound(0, startPos, finalPos)).index;
995 const int endIndex = d->cursorPos.at(qBound(0, endPos, finalPos)).index;
996 int sought = startIndex;
997 if (startIndex == endIndex) {
998 int currentIndex = 0;
999 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1000 if (it != d->textData.depthFirstTailEnd()) {
1001 if (inherited) {
1002 props.append(inheritProperties(it));
1003 } else {
1004 props.append(it->properties);
1005 }
1006 } else {
1007 currentIndex = 0;
1008 it = d->findTextContentElementForIndex(d->textData, currentIndex, sought - 1);
1009 if (it != d->textData.depthFirstTailEnd()) {
1010 if (inherited) {
1011 props.append(inheritProperties(it));
1012 } else {
1013 props.append(it->properties);
1014 }
1015 }
1016 }
1017 } else {
1018 while(sought < endIndex) {
1019 int currentIndex = 0;
1020 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1021 if (KisForestDetail::siblingCurrent(it) == d->textData.childBegin()) {
1022 // If there's a selection and the search algorithm only returns the root, return empty.
1023 // The root text properties should be retrieved explicitly (either by using -1 as pos, or by calling textProperties()).
1024 props = {KoSvgTextProperties()};
1025 return props;
1026 } else if (it != d->textData.depthFirstTailEnd()) {
1027 if (inherited) {
1028 props.append(inheritProperties(it));
1029 } else {
1030 props.append(it->properties);
1031 }
1032 }
1033 sought = currentIndex + it->numChars(false);
1034 }
1035 }
1036
1037 return props;
1038}
1039
1041{
1042 if (pos < 0 || d->cursorPos.isEmpty()) {
1043 if (KisForestDetail::size(d->textData)) {
1044 d->textData.childBegin()->properties = properties;
1045 }
1046 notifyChanged();
1048 return;
1049 }
1050 CursorPos cursorPos = d->cursorPos.at(pos);
1051 CharacterResult res = d->result.at(cursorPos.cluster);
1052 int currentIndex = 0;
1053 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, res.plaintTextIndex);
1054 if (it != d->textData.depthFirstTailEnd()) {
1055 it->properties = properties;
1056 notifyChanged();
1058 }
1059}
1060
1062 const int endPos,
1063 const KoSvgTextProperties properties,
1064 const QSet<KoSvgTextProperties::PropertyId> removeProperties)
1065{
1066 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1067 if (KisForestDetail::size(d->textData)) {
1068 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1069 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1070 }
1071 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1072 d->textData.childBegin()->properties.removeProperty(p);
1073 }
1074 }
1075 notifyChanged();
1078 || removeProperties.contains(KoSvgTextProperties::ShapePaddingId)
1079 || removeProperties.contains(KoSvgTextProperties::ShapeMarginId)) {
1081 } else {
1083 }
1084 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1086 }
1089 }
1090 return;
1091 }
1092 const int startIndex = d->cursorPos.at(startPos).index;
1093 const int endIndex = d->cursorPos.at(endPos).index;
1094 if (startIndex != endIndex) {
1095 KoSvgTextShape::Private::splitContentElement(d->textData, startIndex);
1096 KoSvgTextShape::Private::splitContentElement(d->textData, endIndex);
1097 }
1098 bool changed = false;
1099 int currentIndex = 0;
1100 KoSvgText::AutoValue inlineSize = d->textData.childBegin()->properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
1101 bool isWrapping = !d->shapesInside.isEmpty() || !inlineSize.isAuto;
1102 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1103 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0) {
1104 continue;
1105 }
1106
1107 if (currentIndex >= startIndex && currentIndex < endIndex) {
1108 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1110 d->textData.childBegin()->properties.removeProperty(p);
1111 } else {
1112 it->properties.removeProperty(p);
1113 }
1114 }
1115 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1117 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1118 } else {
1119 it->properties.setProperty(p, properties.property(p));
1120 }
1121 }
1122
1123 changed = true;
1124 }
1125 currentIndex += it->numChars(false);
1126 }
1127
1128 if (changed){
1129 KoSvgTextShape::Private::cleanUp(d->textData);
1130 notifyChanged();
1132 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1134 }
1137 }
1138 }
1139}
1140
1141std::unique_ptr<KoSvgTextShape> KoSvgTextShape::copyRange(int index, int length) const
1142{
1143 KoSvgTextShape *clone = new KoSvgTextShape(*this);
1144 int zero = 0;
1145 int endRange = index + length;
1146 int size = KoSvgTextShape::Private::numChars(clone->d->textData.childBegin(), false) - endRange;
1147 clone->removeText(endRange, size);
1148 clone->removeText(zero, index);
1149 KoSvgTextShape::Private::cleanUp(clone->d->textData);
1150 return std::unique_ptr<KoSvgTextShape>(clone);
1151}
1152
1153bool KoSvgTextShape::insertRichText(int pos, const KoSvgTextShape *richText, bool inheritPropertiesIfPossible)
1154{
1155 bool success = false;
1156 int currentIndex = 0;
1157 int elementIndex = 0;
1158 int insertionIndex = 0;
1159
1160 if (isEnd(richText->d->textData.childBegin())) {
1161 // rich text is empty.
1162 return success;
1163 }
1164
1165 if (pos > -1 && !d->cursorPos.isEmpty()) {
1166 CursorPos cursorPos = d->cursorPos.at(qMin(d->cursorPos.size()-1, pos));
1167 CharacterResult res = d->result.at(cursorPos.cluster);
1168 elementIndex = res.plaintTextIndex;
1169 insertionIndex = cursorPos.index;
1170 elementIndex = qMin(elementIndex, d->result.size()-1);
1171 }
1172
1173 KoSvgTextShape::Private::splitContentElement(this->d->textData, insertionIndex);
1174
1175 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, insertionIndex);
1176 auto richTextIt = d->textData.childEnd();
1177 if (it != d->textData.depthFirstTailEnd()) {
1178 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingCurrent(it));
1179 success = true;
1180 } else {
1181 currentIndex = 0;
1182 it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
1183 if (it != d->textData.depthFirstTailEnd()) {
1184 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingEnd(siblingCurrent(it)));
1185 success = true;
1186 }
1187 }
1188
1189 if (richTextIt != d->textData.childEnd()) {
1190 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1192 richTextIt->properties.removeProperty(p);
1193 }
1194 }
1195 auto parentIt = KisForestDetail::hierarchyBegin(richTextIt);
1196 auto parentEnd = KisForestDetail::hierarchyEnd(richTextIt);
1197 if (parentIt != parentEnd) {
1198 parentIt++;
1199 if (inheritPropertiesIfPossible && parentIt != parentEnd) {
1200 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1201 if (richTextIt->properties.inheritsProperty(p, parentIt->properties)) {
1202 richTextIt->properties.removeProperty(p);
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209 if (success) {
1210 notifyChanged();
1212 }
1213 return success;
1214}
1215
1217{
1218 KoSvgTextShape::Private::cleanUp(d->textData);
1219 notifyChanged();
1221}
1222
1223bool KoSvgTextShape::setCharacterTransformsOnRange(const int startPos, const int endPos, const QVector<QPointF> positions, const QVector<qreal> rotateDegrees, const bool deltaPosition)
1224{
1225 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1226 return false;
1227 }
1228 const int finalPos = d->cursorPos.size()-1;
1229 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1230 int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1231 if (startIndex == endIndex) {
1232 endIndex += 1;
1233 while(d->result.at(endIndex).middle) {
1234 endIndex += 1;
1235 if (endIndex > d->result.size()) break;
1236 }
1237 }
1238
1239 bool changed = false;
1240
1241 QVector<KoSvgText::CharTransformation> resolvedTransforms = Private::resolvedTransformsForTree(d->textData, !shapesInside().isEmpty(), true);
1242 Private::removeTransforms(d->textData, startIndex, endIndex-startIndex);
1243 QPointF totalStartDelta;
1244 QPointF anchorAbsolute;
1245 QPointF anchorCssPos;
1246
1247 const KoSvgText::Direction dir = KoSvgText::Direction(this->textProperties().propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
1249
1250 const CharacterResult startRes = d->result.value(startIndex);
1251 const QPointF startAdvance = startRes.cursorInfo.rtl? startRes.advance: QPointF();
1252
1253 if (deltaPosition) {
1254 for (int i = 0; i< startIndex; i++) {
1255 KoSvgText::CharTransformation tf = resolvedTransforms.value(i);
1256 if (tf.xPos) {
1257 totalStartDelta.setX(0);
1258 anchorAbsolute.setX(*tf.xPos);
1259 }
1260 if (tf.yPos) {
1261 totalStartDelta.setY(0);
1262 anchorAbsolute.setY(*tf.yPos);
1263 }
1264 if (tf.startsNewChunk()) {
1265 const CharacterResult res = d->result.value(i);
1266 anchorCssPos = res.cssPosition;
1267 }
1268 totalStartDelta += tf.relativeOffset();
1269 }
1270 } else {
1271 bool rtl = (dir == KoSvgText::DirectionRightToLeft);
1272 QPointF positionAtVisualEnd = (rtl? d->result.first(): d->result.last()).finalPosition;
1274 anchorAbsolute = positionAtVisualEnd;
1275 } else if (anchor == KoSvgText::AnchorMiddle) {
1276 anchorAbsolute = positionAtVisualEnd/2;
1277 }
1278 }
1279
1280 int currentIndex = 0;
1281 QPointF accumulatedOffset;
1282 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1283 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0 || !it->textPathId.isEmpty()) {
1284 continue;
1285 }
1286
1287 int endContentElement = it->finalResultIndex;
1288
1289 if (endContentElement >= startIndex && currentIndex <= endIndex) {
1290
1292 int addressableOffset = 0;
1293 for (int i = currentIndex; (i < endContentElement); i++) {
1294 const CharacterResult res = d->result.value(i);
1295 if (!res.addressable) {
1296 addressableOffset += 1;
1297 continue;
1298 }
1299
1300 const int transformIndex = (i - startIndex) - addressableOffset;
1301 KoSvgText::CharTransformation tf = resolvedTransforms.value(i, KoSvgText::CharTransformation());
1302
1303 // Function to get the delta position.
1304 auto getDelta = [res, totalStartDelta, accumulatedOffset, anchorAbsolute, anchorCssPos, startAdvance] (QPointF pos) -> QPointF {
1305 QPointF delta = pos - (res.textPathAndAnchoringOffset + anchorAbsolute + res.textLengthOffset);
1306 delta -= (totalStartDelta + accumulatedOffset + (res.cssPosition-anchorCssPos) + startAdvance);
1307 return delta;
1308 };
1309 // Function to get absolute position.
1310 auto getAbsolute = [res, tf, anchorAbsolute] (QPointF pos) -> QPointF {
1311 QPointF p = pos - (res.textPathAndAnchoringOffset - anchorAbsolute) - tf.relativeOffset();
1312 return p;
1313 };
1314
1315 if (i < startIndex) {
1316 if (!deltaPosition) {
1317 // Because we don't split the text content element, we need to set the absolute pos for every preceding transform.
1318 const QPointF p = getAbsolute(res.finalPosition);
1319 if (!tf.xPos) {
1320 tf.xPos = p.x();
1321 }
1322 if (!tf.yPos) {
1323 tf.yPos = p.y();
1324 }
1325 }
1326 transforms << tf;
1327 continue;
1328 }
1329
1330 if (i >= endIndex) {
1331 if (i == endIndex) {
1332 // Counter transform to keep unselected characters at the same pos.
1333 if (deltaPosition && !tf.startsNewChunk()) {
1334 QPointF delta = getDelta(res.finalPosition);
1335 tf.dxPos = delta.x();
1336 tf.dyPos = delta.y();
1337 } else {
1338 const QPointF p = getAbsolute(res.finalPosition) - anchorAbsolute;
1339 tf.xPos = p.x();
1340 tf.yPos = p.y();
1341 }
1342 if (!tf.rotate) {
1343 tf.rotate = 0.0;
1344 }
1345 }
1346 transforms << tf;
1347 continue;
1348 }
1349
1350 if (rotateDegrees.size()+startIndex > i) {
1351 tf.rotate = kisDegreesToRadians(rotateDegrees.value(transformIndex, tf.rotate? *tf.rotate: rotateDegrees.last()));
1352 }
1353 if (positions.size()+startIndex > i) {
1354 const QPointF pos = positions.value(transformIndex, QPointF());
1355
1356 if (deltaPosition) {
1357 if (tf.startsNewChunk()) {
1358 anchorAbsolute = tf.absolutePos();
1359 totalStartDelta = tf.relativeOffset();
1360 anchorCssPos = res.cssPosition;
1361 }
1362
1363 QPointF delta = getDelta(pos);
1364 tf.dxPos = delta.x();
1365 tf.dyPos = delta.y();
1366
1367 if (tf.startsNewChunk()) {
1368 accumulatedOffset = QPointF();
1369 totalStartDelta = tf.relativeOffset();
1370 } else {
1371 accumulatedOffset += tf.relativeOffset();
1372 }
1373 } else {
1374 const QPointF delta = getAbsolute(pos);
1375 tf.xPos = delta.x();
1376 tf.yPos = delta.y();
1377 accumulatedOffset = pos - res.finalPosition;
1378 }
1379
1380 }
1381
1382 transforms << tf;
1383 }
1384 it->localTransformations = transforms;
1385 changed = true;
1386 }
1387 currentIndex = it->finalResultIndex;
1388 if (currentIndex > endIndex) {
1389 break;
1390 }
1391 }
1392 const CharacterResult res = d->result.last();
1393
1394 if (changed) {
1395 KoSvgTextShape::Private::cleanUp(d->textData);
1396 notifyChanged();
1398 }
1399
1400 return changed;
1401}
1402
1405 info.finalPos = res.finalPosition;
1407 info.visualIndex = res.visualIndex;
1408 info.middle = res.middle;
1409 info.advance = res.advance;
1410 info.logicalIndex = index;
1411 info.rtl = res.cursorInfo.rtl;
1412 info.metrics = res.metrics;
1413 return info;
1414}
1415
1417{
1419 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1420 return infos;
1421 }
1422 const int finalPos = d->cursorPos.size()-1;
1423 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1424 const int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1425
1426 for (int i = startIndex; i < endIndex; i++) {
1427 CharacterResult res = d->result.value(i);
1428 if (!res.addressable) continue;
1429 infos << infoFromCharacterResult(res, i);
1430 }
1431
1432 if (endIndex == startIndex) {
1433 bool final = qMax(startPos, endPos) == finalPos;
1434 CharacterResult resFinal = final? d->result.last(): d->result.value(startIndex);
1435 if (resFinal.addressable) {
1436 infos << infoFromCharacterResult(resFinal, startIndex);
1437 }
1438 }
1439
1440 return infos;
1441}
1442
1443void KoSvgTextShape::removeTransformsFromRange(const int startPos, const int endPos)
1444{
1445 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1446 return;
1447 }
1448 const int finalPos = d->cursorPos.size()-1;
1449 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1450 const int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1451
1452 d->removeTransforms(d->textData, startIndex, endIndex-startIndex);
1453
1454 KoSvgTextShape::Private::cleanUp(d->textData);
1455 notifyChanged();
1457}
1458
1460 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1461 if (child->properties.hasProperty(propertyId)) {
1462 return child;
1463 } else if (KisForestDetail::childBegin(child) != KisForestDetail::childEnd(child)) {
1464 auto found = findNodeIndexForPropertyIdImpl(child, propertyId);
1465 if (found != child) {
1466 return found;
1467 }
1468 }
1469 }
1470 return parent;
1471}
1472
1474{
1475 for (auto it = d->textData.childBegin(); it != d->textData.childEnd(); it++) {
1476 if (it->properties.hasProperty(propertyId)) {
1477 return d->createTextNodeIndex(it);
1479 auto found = findNodeIndexForPropertyIdImpl(it, propertyId);
1480 if (found != it) {
1481 return d->createTextNodeIndex(found);
1482 }
1483 }
1484 }
1485 return d->createTextNodeIndex(d->textData.childBegin());
1486}
1487
1489{
1490 int startIndex = 0;
1491 int endIndex = 0;
1492 for (auto child = d->textData.childBegin(); child != d->textData.childEnd(); child++) {
1493 // count children
1494 d->startIndexOfIterator(child, node.d->textElement, startIndex);
1495 endIndex = d->numChars(node.d->textElement) + startIndex;
1496 }
1497 return qMakePair(posForIndex(startIndex), posForIndex(endIndex));
1498}
1499
1501{
1502 auto candidate = d->textData.childBegin();
1503 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(candidate);
1504 if (childBegin(d->textData.childBegin()) != childEnd(d->textData.childBegin())) {
1505 candidate = childBegin(d->textData.childBegin());
1506 }
1507 const int finalPos = d->cursorPos.size() - 1;
1508 const int index = d->cursorPos.at(qBound(0, pos, finalPos)).index;
1509 int currentIndex = 0;
1510
1511 auto e = Private::findTextContentElementForIndex(d->textData, currentIndex, index, true);
1512 if (e == d->textData.depthFirstTailEnd()) {
1513 return d->createTextNodeIndex(candidate);
1514 }
1515
1516 auto element = KisForestDetail::siblingCurrent(e);
1517 auto parent = Private::findTopLevelParent(d->textData.childBegin(), element);
1518 if (parent == childEnd(d->textData.childBegin())) return d->createTextNodeIndex(candidate);
1519
1520 return d->createTextNodeIndex(parent);
1521}
1522
1524{
1525 auto root = d->textData.childBegin();
1526 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(root);
1527
1528 for (auto child = childBegin(root); child != childEnd(root); child++) {
1529 if (child->textPathId == textPath->name()) {
1530 return d->createTextNodeIndex(child);
1531 }
1532 }
1533 return d->createTextNodeIndex(root);
1534}
1535
1537{
1538 return KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1539}
1540
1549
1551{
1552 if (KisForestDetail::size(d->textData) == 0) {
1553 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1554 }
1555 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::FillId,
1556 QVariant::fromValue(KoSvgText::BackgroundProperty(background)));
1557
1559 notifyChanged();
1560}
1561
1563{
1564 KoSvgTextProperties props = KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1566}
1567
1569{
1570 if (KisForestDetail::size(d->textData) == 0) {
1571 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1572 }
1573 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::StrokeId,
1574 QVariant::fromValue(KoSvgText::StrokeProperty(stroke)));
1576 notifyChanged();
1577}
1578
1587
1589{
1590 if (KisForestDetail::size(d->textData) == 0) {
1591 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1592 }
1593 KIS_SAFE_ASSERT_RECOVER_RETURN(first != second);
1595
1596 if (first != Fill) {
1597 if (order.at(1) == first) {
1598 order[1] = order[0];
1599 order[0] = first;
1600 } else if (order.at(2) == first) {
1601 order[2] = order[0];
1602 order[0] = first;
1603 }
1604 }
1605 if (second != first && second != Stroke) {
1606 if (order.at(2) == second) {
1607 order[2] = order[1];
1608 order[1] = second;
1609 }
1610 }
1611 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::PaintOrder,
1612 QVariant::fromValue(order));
1613 setInheritPaintOrder(false);
1614}
1615
1617{
1618 return d->plainText;
1619}
1620
1625
1627{
1628 if (d->textData.empty()) return false;
1629 return (KisForestDetail::size(d->textData) == 1);
1630}
1631
1633{
1634 if (d->isLoading) {
1635 return;
1636 }
1637 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1638 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1639 if (cursorListener) {
1640 cursorListener->notifyCursorPosChanged(pos, anchor);
1641 }
1642 }
1643}
1644
1646{
1647 if (d->isLoading) {
1648 return;
1649 }
1650 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1651 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1652 if (cursorListener) {
1653 cursorListener->notifyMarkupChanged();
1654 }
1655 }
1656}
1657
1659{
1660 const int inlineSize = writingMode() == KoSvgText::HorizontalTB? outlineRect().width(): outlineRect().height();
1661 d->applyWhiteSpace(d->textData, true);
1662 d->insertNewLinesAtAnchors(d->textData, !d->shapesInside.isEmpty());
1663 d->cleanUp(d->textData);
1664
1665 KoSvgTextProperties props = this->propertiesForPos(-1);
1666 if (makeInlineSize) {
1668 // Using QCeil here because otherwise the text will layout too tight.
1669 val.customValue = qCeil(inlineSize);
1670 val.isAuto = false;
1672 props.setProperty(KoSvgTextProperties::InlineSizeId, QVariant::fromValue(val));
1673 }
1674 } else {
1676 }
1677 // NOTE: applyWhiteSpace and insertNewLines don't notify changes,
1678 // so setProperties is the only thing triggering relayout();
1679 setPropertiesAtPos(-1, props);
1680}
1681
1683{
1684 if (d->result.isEmpty()) return;
1685 d->setTransformsFromLayout(d->textData, d->result);
1686 d->cleanUp(d->textData);
1687 //d->applyWhiteSpace(d->textData, true);
1688 KoSvgTextProperties props = this->propertiesForPos(-1);
1692
1693 setPropertiesAtPos(-1, props);
1694}
1695
1696#include "KoXmlWriter.h"
1697#include "SvgWriter.h"
1699{
1700 bool success = false;
1701
1702 QList<KoShape*> visibleShapes;
1703 Q_FOREACH(KoShape *shape, d->internalShapes()) {
1704 if (shape->isVisible(false)) {
1705 visibleShapes.append(shape);
1706 }
1707 }
1708 const bool writeGroup = !(visibleShapes.isEmpty() || context.strippedTextMode());
1709 if (writeGroup) {
1710 context.shapeWriter().startElement("g", false);
1711 context.shapeWriter().addAttribute("id", context.createUID("group"));
1712 context.shapeWriter().addAttribute(KoSvgTextShape_TEXTCONTOURGROUP, "true");
1713
1715 SvgWriter writer(visibleShapes);
1716 writer.saveDetached(context);
1717 }
1718 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1719 if (it.state() == KisForestDetail::Enter) {
1720 bool isTextPath = false;
1721 QMap<QString, QString> shapeSpecificStyles;
1722 if (!it->textPathId.isEmpty()) {
1723 isTextPath = true;
1724 }
1725 if (it == d->textData.compositionBegin()) {
1726 context.shapeWriter().startElement("text", false);
1727
1728 if (!context.strippedTextMode()) {
1729 context.shapeWriter().addAttribute("id", context.getID(this));
1730
1731 // save the version to distinguish from the buggy Krita version
1732 // 2: Wrong font-size.
1733 // 3: Wrong font-size-adjust.
1734 context.shapeWriter().addAttribute("krita:textVersion", 3);
1735
1736 if (visibleShapes.isEmpty()) {
1738 }
1739 SvgStyleWriter::saveSvgStyle(this, context);
1740 } else {
1741 SvgStyleWriter::saveSvgFill(this->background(), false, this->outlineRect(), this->size(), this->absoluteTransformation(), context);
1742 SvgStyleWriter::saveSvgStroke(this->stroke(), context);
1744 inheritPaintOrder(), context, true);
1745 }
1746 shapeSpecificStyles = this->shapeTypeSpecificStyles(context);
1747 } else {
1748 if (isTextPath) {
1749 context.shapeWriter().startElement("textPath", false);
1750 } else {
1751 context.shapeWriter().startElement("tspan", false);
1752 }
1753 SvgStyleWriter::saveSvgBasicStyle(it->properties.property(KoSvgTextProperties::Visibility, true).toBool(),
1754 it->properties.property(KoSvgTextProperties::Opacity, 0).toReal(),
1755 it->properties.property(KoSvgTextProperties::PaintOrder,
1756 QVariant::fromValue(paintOrder())
1758 !it->properties.hasProperty(KoSvgTextProperties::PaintOrder), context, true);
1759
1760 }
1761
1762 KoShape *textPath = KoSvgTextShape::Private::textPathByName(it->textPathId, d->textPaths);
1763 success = it->saveSvg(context,
1764 it == d->textData.compositionBegin(),
1765 d->childCount(siblingCurrent(it)) == 0,
1766 shapeSpecificStyles,
1767 textPath);
1768 } else {
1769 if (it == d->textData.compositionBegin()) {
1770 SvgStyleWriter::saveMetadata(this, context);
1771 }
1772 context.shapeWriter().endElement();
1773 }
1774 }
1775 if (writeGroup) {
1776 context.shapeWriter().endElement();
1777 }
1778 return success;
1779}
1780
1782{
1783 bool success = true;
1785 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1786 if (it.state() == KisForestDetail::Enter) {
1787 QMap<QString, QString> shapeSpecificStyles;
1788
1789 if (it == d->textData.compositionBegin()) {
1790 context.shapeWriter().startElement("p", false);
1791 } else {
1792 context.shapeWriter().startElement("span", false);
1793 }
1794 KoSvgTextProperties ownProperties = it->properties.ownProperties(parentProps.last(),
1795 it == d->textData.compositionBegin());
1796 parentProps.append(ownProperties);
1797 QMap<QString, QString> attributes = ownProperties.convertToSvgTextAttributes();
1798 if (it == d->textData.compositionBegin())
1799 attributes.insert(ownProperties.convertParagraphProperties());
1800 bool addedFill = false;
1801 if (attributes.size() > 0) {
1802 QString styleString;
1803 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
1804 if (QString(it.key().toLatin1().data()).contains("text-anchor")) {
1805 QString val = it.value();
1806 if (it.value()=="middle") {
1807 val = "center";
1808 } else if (it.value()=="end") {
1809 val = "right";
1810 } else {
1811 val = "left";
1812 }
1813 styleString.append("text-align")
1814 .append(": ")
1815 .append(val)
1816 .append(";" );
1817 } else if (QString(it.key().toLatin1().data()).contains("fill")) {
1818 styleString.append("color")
1819 .append(": ")
1820 .append(it.value())
1821 .append(";" );
1822 addedFill = true;
1823 } else if (QString(it.key().toLatin1().data()).contains("font-size")) {
1824 QString val = it.value();
1825 styleString.append(it.key().toLatin1().data())
1826 .append(": ")
1827 .append(val)
1828 .append(";" );
1829 } else {
1830 styleString.append(it.key().toLatin1().data())
1831 .append(": ")
1832 .append(it.value())
1833 .append(";" );
1834 }
1835 }
1836 if (ownProperties.hasProperty(KoSvgTextProperties::FillId) && !addedFill) {
1837 KoColorBackground *b = dynamic_cast<KoColorBackground *>(it->properties.background().data());
1838 if (b) {
1839 styleString.append("color")
1840 .append(": ")
1841 .append(b->color().name())
1842 .append(";" );
1843 }
1844 }
1845 context.shapeWriter().addAttribute("style", styleString);
1846
1847 if (d->childCount(siblingCurrent(it)) == 0) {
1848 debugFlake << "saveHTML" << this << it->text;
1849 // After adding all the styling to the <p> element, add the text
1850 context.shapeWriter().addTextNode(it->text);
1851 }
1852 }
1853 } else {
1854 parentProps.pop_back();
1855 context.shapeWriter().endElement();
1856 }
1857 }
1858 return success;
1859}
1860
1862{
1863
1864}
1865
1867{
1868
1869}
1870
1872{
1874 d->result,
1875 d->lineBoxes,
1876 d->cursorPos,
1877 d->logicalToVisualCursorPos,
1878 d->plainText,
1879 d->isBidi,
1880 d->initialTextPosition));
1881}
1882
1884{
1885 // TODO: add an assert that all linked shpaes in memento are present in
1886 // the current state of d->textPaths. That is the responsibility of
1887 // KoSvgTextAddRemoveShapeCommandImpl to prepare the shapes for us
1888
1889 KoSvgTextShapeMementoImpl *impl = dynamic_cast<KoSvgTextShapeMementoImpl*>(memento.data());
1890 if (impl) {
1891 d->textData = impl->textData;
1892 d->result = impl->result;
1893 d->lineBoxes = impl->lineBoxes;
1894 d->cursorPos = impl->cursorPos;
1895 d->logicalToVisualCursorPos = impl->logicalToVisualCursorPos;
1896 d->plainText = impl->plainText;
1897 d->isBidi = impl->isBidi;
1898 d->initialTextPosition = impl->initialTextPosition;
1899
1900 // Ensure that any text paths exist.
1901 auto root = d->textData.childBegin();
1902 for (auto child = childBegin(root); child != childEnd(root); child++) {
1903 if (!child->textPathId.isEmpty()) {
1904 KIS_SAFE_ASSERT_RECOVER(Private::textPathByName(child->textPathId, d->textPaths)) {
1905 qDebug() << "missing path is" << child->textPathId;
1906 child->textPathId = QString();
1907 }
1908 }
1909 }
1910 }
1911}
1912
1919
1920void KoSvgTextShape::setMemento(const KoSvgTextShapeMementoSP memento, int pos, int anchor)
1921{
1922 const bool shapeOffsetBefore = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1924 setMementoImpl(memento);
1925 const bool shapeOffsetAfter = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1927 if (shapeOffsetBefore || shapeOffsetAfter) {
1928 d->updateTextWrappingAreas();
1929 }
1932}
1933
1935{
1936 qDebug() << "Tree size:" << KisForestDetail::size(d->textData);
1937 QString spaces;
1938 for (auto it = compositionBegin(d->textData); it != compositionEnd(d->textData); it++) {
1939 if (it.state() == KisForestDetail::Enter) {
1940
1941 qDebug() << QString(spaces + "+") << it->text;
1942 qDebug() << QString(spaces + "|") << it->properties.convertToSvgTextAttributes();
1943 qDebug() << QString(spaces + "| PropertyType:") << it->properties.property(KoSvgTextProperties::KraTextStyleType).toString();
1944 qDebug() << QString(spaces + "| Fill set: ") << it->properties.hasProperty(KoSvgTextProperties::FillId);
1945 qDebug() << QString(spaces + "| Stroke set: ") << it->properties.hasProperty(KoSvgTextProperties::StrokeId);
1946 qDebug() << QString(spaces + "| Opacity: ") << it->properties.property(KoSvgTextProperties::Opacity);
1947 qDebug() << QString(spaces + "| PaintOrder: ") << it->properties.hasProperty(KoSvgTextProperties::PaintOrder);
1948 qDebug() << QString(spaces + "| Visibility set: ") << it->properties.hasProperty(KoSvgTextProperties::Visibility);
1949 qDebug() << QString(spaces + "| TextPath set: ") << it->textPathId;
1950 qDebug() << QString(spaces + "| Transforms set: ") << it->localTransformations;
1951 spaces.append(" ");
1952 }
1953
1954 if (it.state() == KisForestDetail::Leave) {
1955 spaces.chop(1);
1956 }
1957 }
1958}
1959
1961{
1962 d->isLoading = disable;
1963}
1964
1966{
1967 return d->isLoading;
1968}
1969
1971{
1972 d->disableFontMatching = disable;
1973}
1974
1976{
1977 return d->disableFontMatching;
1978}
1979
1981{
1982 return Private::generateShapes(shapesInside, shapesSubtract, props);
1983}
1984
1986{
1987 KIS_SAFE_ASSERT_RECOVER_RETURN(!d->bulkActionState);
1988 d->bulkActionState.emplace(boundingRect());
1989}
1990
1992{
1994
1995 QRectF updateRect;
1996
1997 if (d->bulkActionState->changed()) {
1998 if (d->bulkActionState->contourHasChanged) {
1999 d->updateTextWrappingAreas();
2000 } else if (d->bulkActionState->layoutHasChanged) {
2001 // updateTextWrappingAreas() already includes a call to relayout()
2002 relayout();
2003 }
2004
2005 updateRect = d->bulkActionState->originalBoundingRect | boundingRect();
2006 }
2007
2008 d->bulkActionState = std::nullopt;
2009 return updateRect;
2010}
2011
2012void KoSvgTextShape::paint(QPainter &painter) const
2013{
2014 painter.save();
2015
2016 painter.setTransform(d->shapeGroup->absoluteTransformation().inverted()*painter.transform());
2017 d->internalShapesPainter->paint(painter);
2018 painter.restore();
2019
2020 painter.save();
2022 if (textRendering == KoSvgText::RenderingOptimizeSpeed || !painter.testRenderHint(QPainter::Antialiasing)) {
2023 // also apply antialiasing only if antialiasing is active on provided target QPainter
2024 painter.setRenderHint(QPainter::Antialiasing, false);
2025 painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
2026 } else {
2027 painter.setRenderHint(QPainter::Antialiasing, true);
2028 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
2029 }
2030
2031 QPainterPath chunk;
2032 int currentIndex = 0;
2033 if (!d->result.isEmpty()) {
2034 QPainterPath rootBounds;
2035 rootBounds.addRect(this->outline().boundingRect());
2036 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationUnderline, textRendering);
2037 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationOverline, textRendering);
2038 d->paintPaths(painter, rootBounds, this, d->result, textRendering, chunk, currentIndex);
2039 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationLineThrough, textRendering);
2040 }
2041
2042 painter.restore();
2043}
2044
2045void KoSvgTextShape::paintStroke(QPainter &painter) const
2046{
2047 Q_UNUSED(painter);
2048 // do nothing! everything is painted in paint()
2049}
2050
2051QPainterPath KoSvgTextShape::outline() const {
2052 QPainterPath result;
2053 if (!d->internalShapes().isEmpty()) {
2054 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2055 result.addPath(shape->transformation().map(shape->outline()));
2056 }
2057 }
2058
2059 if ((d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2060 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2061 result.addPath(it->associatedOutline);
2062 for (int i = 0; i < it->textDecorations.values().size(); ++i) {
2063 result.addPath(it->textDecorations.values().at(i));
2064 }
2065 }
2066 }
2067
2068 return result;
2069}
2071{
2072 return outline().boundingRect();
2073}
2074
2076{
2077 QRectF shapesRect;
2078 if (d->internalShapesPainter->contentRect().isValid()) {
2079 shapesRect = d->internalShapesPainter->contentRect();
2080 if (!(d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2081 return shapesRect;
2082 }
2083 }
2084 QRectF result;
2085
2086 QList<KoShapeStrokeModelSP> parentStrokes;
2087 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
2088 if (it.state() == KisForestDetail::Enter) {
2089 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
2090 parentStrokes.append(it->properties.property(KoSvgTextProperties::StrokeId).value<KoSvgText::StrokeProperty>().property);
2091 }
2092 } else {
2093 KoShapeStrokeModelSP stroke = parentStrokes.size() > 0? parentStrokes.last(): nullptr;
2094 QRectF bb = it->associatedOutline.boundingRect();
2095 QMap<KoSvgText::TextDecoration, QPainterPath> decorations = it->textDecorations;
2096 for (int i = 0; i < decorations.values().size(); ++i) {
2097 bb |= decorations.values().at(i).boundingRect();
2098 }
2099 if (!bb.isEmpty()) {
2100 if (stroke) {
2101 KoInsets insets;
2102 stroke->strokeInsets(this, insets);
2103 result |= bb.adjusted(-insets.left, -insets.top, insets.right, insets.bottom);
2104 } else {
2105 result |= bb;
2106 }
2107 }
2108 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
2109 // reset stroke to use parent stroke.
2110 parentStrokes.pop_back();
2111 }
2112 }
2113 }
2114
2115 return (this->absoluteTransformation().mapRect(result) | shapesRect);
2116}
2117
2119{
2120 // TODO: check if KoShape::m_d->size is consistent, check cache in KoShapeGroup::size()
2121 return outlineRect().size();
2122}
2123
2124void KoSvgTextShape::setSize(const QSizeF &size)
2125{
2126 const QRectF oRect = this->outlineRect();
2127 const QSizeF oldSize = oRect.size();
2128
2129 if (size == oldSize) return;
2130
2131 // don't try to divide by zero
2132 if (oldSize.isEmpty()) return;
2133
2134 const qreal scaleX = size.width() / oldSize.width();
2135 const qreal scaleY = size.height() / oldSize.height();
2136
2137 if (d->internalShapes().isEmpty()) {
2143 const qreal scaleX = size.width() / oldSize.width();
2144 const qreal scaleY = size.height() / oldSize.height();
2145
2146 this->scale(scaleX, scaleY);
2147 // TODO: use scaling function for kosvgtextproperties when styles presets are merged.
2148 notifyChanged();
2150 } else {
2151 if (!d->textPaths.isEmpty()) return;
2152
2153 const bool allInternalShapeAreTranslatedOnly = [this] () {
2154 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2155 if (shape->transformation().type() > QTransform::TxTranslate) {
2156 return false;
2157 }
2158 }
2159 return true;
2160 }();
2161
2162 if (allInternalShapeAreTranslatedOnly) {
2167 Q_FOREACH (KoShape *internalShape, d->internalShapes()) {
2174 const QPointF stillPoint = this->absoluteTransformation().map(QPointF());
2175 const bool useGlobalMode = false;
2176 const bool usePostScaling = false;
2177 KoFlake::resizeShapeCommon(internalShape,
2178 scaleX,
2179 scaleY,
2180 stillPoint,
2181 useGlobalMode,
2182 usePostScaling,
2183 QTransform());
2184 }
2185
2186 const QSizeF realNewSize = outlineRect().size();
2187 KoShape::setSize(realNewSize);
2188 } else {
2232 const QTransform scale = QTransform::fromScale(scaleX, scaleY);
2233
2234 Q_FOREACH (KoShape *shape, d->internalShapes()) {
2235 shape->setTransformation(shape->transformation() * scale);
2236 }
2237
2238 const QSizeF realNewSize = outlineRect().size();
2239 KoShape::setSize(realNewSize);
2240 }
2241 }
2242}
2243
2244void KoSvgTextShape::paintDebug(QPainter &painter, const DebugElements elements) const
2245{
2246 if (elements & DebugElement::CharBbox) {
2247 int currentIndex = 0;
2248 if (!d->result.isEmpty()) {
2249 QPainterPath rootBounds;
2250 rootBounds.addRect(this->outline().boundingRect());
2251 d->paintDebug(painter, d->result, currentIndex);
2252 }
2253
2254 //Debug shape outlines.
2255 Q_FOREACH (KoShape *shapeInside, d->shapesInside) {
2256 QPainterPath p = shapeInside->outline();
2257 p = shapeInside->transformation().map(p);
2258 painter.strokePath(p, QPen(Qt::green));
2259 }
2260 Q_FOREACH (KoShape *shapeInside, d->shapesSubtract) {
2261 QPainterPath p = shapeInside->outline();
2262 p = shapeInside->transformation().map(p);
2263 painter.strokePath(p, QPen(Qt::red));
2264 }
2265 }
2266
2267 if (elements & DebugElement::LineBox) {
2268 Q_FOREACH (LineBox lineBox, d->lineBoxes) {
2269 Q_FOREACH (const LineChunk &chunk, lineBox.chunks) {
2270 QPen pen;
2271 pen.setCosmetic(true);
2272 pen.setWidth(2);
2273 painter.setBrush(QBrush(Qt::transparent));
2274 pen.setColor(QColor(0, 128, 255, 128));
2275 painter.setPen(pen);
2276 painter.drawLine(chunk.length);
2277 pen.setColor(QColor(255, 128, 0, 128));
2278 painter.setPen(pen);
2279 painter.drawRect(chunk.boundingBox);
2280
2281 pen.setColor(QColor(255, 0, 0, 128));
2282 pen.setStyle(Qt::DashDotDotLine);
2283 painter.setPen(pen);
2284 painter.drawLine(chunk.length.translated(lineBox.baselineTop));
2285 pen.setColor(QColor(0, 128, 0, 128));
2286 pen.setStyle(Qt::DashDotLine);
2287 painter.setPen(pen);
2288 painter.drawLine(chunk.length.translated(lineBox.baselineBottom));
2289 }
2290 }
2291 }
2292}
2293
2295{
2296 KoShape *shape = nullptr;
2297 int currentIndex = 0;
2298 if (!d->result.empty()) {
2299 shape = d->collectPaths(this, d->result, currentIndex);
2300 }
2301
2302 return shape;
2303}
2304
2306{
2307 KoSvgText::AutoValue inlineSize = d->textData.childBegin()->properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
2308 if (!d->shapesInside.isEmpty()) {
2309 return TextType::TextInShape;
2310 } else if (!inlineSize.isAuto) {
2311 return TextType::InlineWrap;
2312 } else {
2313 bool textSpaceCollapse = false;
2314 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2315 KoSvgText::TextSpaceCollapse collapse = KoSvgText::TextSpaceCollapse(it->properties.propertyOrDefault(KoSvgTextProperties::TextCollapseId).toInt());
2316 if (collapse == KoSvgText::Collapse || collapse == KoSvgText::PreserveSpaces) {
2317 textSpaceCollapse = true;
2318 break;
2319 }
2320 if (!it->localTransformations.isEmpty()) {
2321 textSpaceCollapse = true;
2322 break;
2323 }
2324 }
2326 }
2328}
2329
2331{
2332 removeShapesFromContours(d->shapesInside, false);
2334}
2335
2337{
2338 Q_FOREACH(KoShape *shape, shapes) {
2339 if (d->textPaths.contains(shape)) {
2340 d->removeTextPathId(d->textData.childBegin(), shape->name());
2341 d->cleanUp(d->textData);
2342 d->textPaths.removeAll(shape);
2343 }
2344
2345 if (inside) {
2346 if (d->shapesSubtract.contains(shape)) {
2347 d->shapesSubtract.removeAll(shape);
2348 }
2349 d->shapesInside.append(shape);
2350 } else {
2351 if (d->shapesInside.contains(shape)) {
2352 d->shapesInside.removeAll(shape);
2353 }
2354 d->shapesSubtract.append(shape);
2355 }
2356 if (!d->shapeGroup->shapes().contains(shape)) {
2357 d->shapeGroup->addShape(shape);
2358 shape->addDependee(this);
2359 }
2360 }
2361
2362 notifyChanged(); // notify shape manager that our geometry has changed
2363
2364 if (!d->isLoading) {
2365 d->updateTextWrappingAreas();
2366 }
2367
2368 d->updateInternalShapesList();
2370 update();
2371}
2372
2374{
2375 return (shape->parent() == d->shapeGroup.data());
2376}
2377
2378void KoSvgTextShape::removeShapesFromContours(QList<KoShape *> shapes, bool callUpdate, bool cleanup)
2379{
2380 Q_FOREACH(KoShape *shape, shapes) {
2381 if (shape) {
2382 d->removeTextPathId(d->textData.childBegin(), shape->name());
2383 shape->removeDependee(this);
2384 d->shapesInside.removeAll(shape);
2385 d->shapesSubtract.removeAll(shape);
2386 d->textPaths.removeAll(shape);
2387
2388 }
2389 d->shapeGroup->removeShape(shape);
2390 }
2391 if (cleanup) {
2392 d->cleanUp(d->textData);
2393 }
2394 if (callUpdate) {
2395 notifyChanged(); // notify shape manager that our geometry has changed
2396 if (!d->isLoading) {
2397 d->updateTextWrappingAreas();
2398 }
2399 d->updateInternalShapesList();
2401 update();
2402 }
2403}
2404
2405void KoSvgTextShape::moveShapeInsideToIndex(KoShape *shapeInside, const int index)
2406{
2407 const int oldIndex = d->shapesInside.indexOf(shapeInside);
2408 if (oldIndex < 0) return;
2409
2410 // Update.
2411 d->shapesInside.move(oldIndex, index);
2412 if (!d->isLoading) {
2413 d->updateTextWrappingAreas();
2414 }
2415 d->updateInternalShapesList();
2417 update();
2418}
2419
2420bool KoSvgTextShape::setTextPathOnRange(KoShape *textPath, const int startPos, const int endPos)
2421{
2422 const int finalPos = d->cursorPos.size() - 1;
2423 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->cursorPos.at(qBound(0, startPos, finalPos)).index;
2424 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->cursorPos.at(qBound(0, endPos, finalPos)).index;
2425
2426 Private::splitTree(d->textData, startIndex, false);
2427 Private::splitTree(d->textData, endIndex, true);
2428 int currentIndex = 0;
2429
2430 Private::makeTextPathNameUnique(d->textPaths, textPath);
2431 KoSvgTextContentElement textPathElement;
2432 textPathElement.textPathId = textPath->name();
2433 d->shapeGroup->addShape(textPath);
2434 d->textPaths.append(textPath);
2435 textPath->addDependee(this);
2436
2437
2438 if (KisForestDetail::depth(d->textData) == 1) {
2439 textPathElement.text = d->textData.childBegin()->text;
2443 Q_FOREACH(KoSvgTextProperties::PropertyId p, copyIds) {
2444 if (d->textData.childBegin()->properties.hasProperty(KoSvgTextProperties::TextDecorationLineId)) {
2445 textPathElement.properties.setProperty(p, d->textData.childBegin()->properties.property(p));
2446 d->textData.childBegin()->properties.removeProperty(p);
2447 }
2448 }
2449
2450 d->textData.childBegin()->text = QString();
2451 d->textData.insert(childEnd(d->textData.childBegin()), textPathElement);
2452 } else {
2453 // find nodes
2454 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2455 currentIndex = 0;
2456 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2457
2458 auto first = startElement.node()?Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2459 : childEnd(d->textData.childBegin());
2460 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2461 : childEnd(d->textData.childBegin());
2462
2463 // move children. we collect them before inserting the text path,
2464 // so we don't get iterator issues.
2466 auto textPathIt = textPathTree.insert(
2467 textPathTree.childEnd(),
2468 textPathElement);
2470 for (auto child = first;
2471 (child != last && child != childEnd(d->textData.childBegin()));
2472 child++) {
2473 if (!child->textPathId.isEmpty()) {
2474 Q_FOREACH(KoShape *shape, d->textPaths) {
2475 if (shape->name() == child->textPathId) {
2476 removeShapesFromContours({shape}, false, false);
2477 break;
2478 }
2479 }
2480 child->textPathId = QString();
2481 }
2482 movableChildren.append(child);
2483 }
2484 while (!movableChildren.isEmpty()) {
2485 auto child = movableChildren.takeLast();
2486 textPathTree.move(child, KisForestDetail::childBegin(textPathIt));
2487 }
2488 d->textData.move(textPathIt, last);
2489 }
2490 Private::cleanUp(d->textData);
2491
2492 d->updateInternalShapesList();
2493 notifyChanged();
2495 update();
2496 return true;
2497}
2498
2499QList<KoShape *> KoSvgTextShape::textPathsAtRange(const int startPos, const int endPos)
2500{
2501 const int finalPos = d->cursorPos.size() - 1;
2502 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->cursorPos.at(qBound(0, startPos, finalPos)).index;
2503 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->cursorPos.at(qBound(0, endPos, finalPos)).index;
2505 int currentIndex = 0;
2506 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2507 currentIndex = 0;
2508 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2509
2510 auto first = startElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2511 : childEnd(d->textData.childBegin());
2512 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2513 : childEnd(d->textData.childBegin());
2514 if (last != childEnd(d->textData.childBegin())) {
2515 last++;
2516 }
2517 for (auto child = first; (child != last && child != KisForestDetail::siblingEnd(first)); child++) {
2518 if (KoShape *path = Private::textPathByName(child->textPathId, d->textPaths)) {
2519 textPaths.append(path);
2520 }
2521 }
2522 return textPaths;
2523}
2524
2526{
2527 auto root = d->textData.childBegin();
2528 if (root == d->textData.childEnd()) return;
2529 if (d->textPaths.contains(textPath)) return;
2530
2531 Private::makeTextPathNameUnique(d->textPaths, textPath);
2532 KoSvgTextContentElement textPathElement;
2533 textPathElement.textPathId = textPath->name();
2534 d->shapeGroup->addShape(textPath);
2535 d->textPaths.append(textPath);
2536 textPath->addDependee(this);
2537
2538 d->textData.insert(childEnd(root), textPathElement);
2539}
2540
2542{
2543 return d->shapesInside;
2544}
2545
2547{
2548 removeShapesFromContours(d->shapesSubtract, false);
2550}
2551
2553{
2554 return d->currentTextWrappingAreas;
2555}
2556
2558{
2559 return d->shapesSubtract;
2560}
2561
2563{
2564 return d->internalShapesPainter->internalShapeManager();
2565}
2566
2567QMap<QString, QString> KoSvgTextShape::shapeTypeSpecificStyles(SvgSavingContext &context) const
2568{
2569 QMap<QString, QString> map = this->textProperties().convertParagraphProperties();
2570 if (!d->shapesInside.isEmpty()) {
2571 QStringList shapesInsideList;
2572 Q_FOREACH(KoShape* shape, d->shapesInside) {
2573 QString id = (shape->isVisible(false) && !context.strippedTextMode())? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2574 shapesInsideList.append(QString("url(#%1)").arg(id));
2575 }
2576 map.insert("shape-inside", shapesInsideList.join(" "));
2579 map.insert("overflow", "clip");
2580 }
2581 if (!d->shapesSubtract.isEmpty()) {
2582 QStringList shapesInsideList;
2583 Q_FOREACH(KoShape* shape, d->shapesSubtract) {
2584 QString id = shape->isVisible(false)? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2585 shapesInsideList.append(QString("url(#%1)").arg(id));
2586 }
2587 map.insert("shape-subtract", shapesInsideList.join(" "));
2588 }
2589
2590 return map;
2591}
2592
2594{
2595 d->relayout();
2596}
2597
2599 : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18nc("Text label in SVG Text Tool", "Text"))
2600{
2601 setToolTip(i18n("SVG Text Shape"));
2602 setIconName(koIconNameCStr("x-shape-text"));
2605
2607 t.name = i18n("SVG Text");
2608 t.iconName = koIconName("x-shape-text");
2609 t.toolTip = i18n("SVG Text Shape");
2610 addTemplate(t);
2611}
2612
2614{
2615 Q_UNUSED(documentResources);
2616 debugFlake << "Create default svg text shape";
2617
2618 KoSvgTextShape *shape = new KoSvgTextShape();
2620 shape->insertText(0, i18nc("Default text for the text shape", "Placeholder Text"));
2621
2622 return shape;
2623}
2624
2626{
2627 KoSvgTextShape *shape = new KoSvgTextShape();
2629
2630 QString svgText = params->stringProperty("svgText", i18nc("Default text for the text shape", "<text>Placeholder Text</text>"));
2631 QString defs = params->stringProperty("defs" , "<defs/>");
2632 QRectF shapeRect = QRectF(0, 0, 200, 60);
2633 QVariant rect = params->property("shapeRect");
2634 QVariant origin = params->property("origin");
2635
2636 if (rect.type()==QVariant::RectF) {
2637 shapeRect = rect.toRectF();
2638 }
2639
2640 KoSvgTextShapeMarkupConverter converter(shape);
2641 converter.convertFromSvg(svgText,
2642 defs,
2643 shapeRect,
2644 documentResources->documentResolution());
2645 if (origin.type() == QVariant::PointF) {
2646 shape->setPosition(origin.toPointF());
2647 } else {
2648 shape->setPosition(shapeRect.topLeft());
2649 }
2651
2652 return shape;
2653}
2654
2655bool KoSvgTextShapeFactory::supports(const QDomElement &/*e*/, KoShapeLoadingContext &/*context*/) const
2656{
2657 return false;
2658}
2659
2661{
2662 Q_UNUSED(type);
2663 Q_UNUSED(shape);
2664}
2665
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
#define debugFlake
Definition FlakeDebug.h:15
float value(const T *src, size_t ch)
const Params2D p
QPointF p2
QList< QString > QStringList
qreal distance(const QPointF &p1, const QPointF &p2)
KisForest< KoSvgTextContentElement >::child_iterator findNodeIndexForPropertyIdImpl(KisForest< KoSvgTextContentElement >::child_iterator parent, KoSvgTextProperties::PropertyId propertyId)
KoSvgTextProperties inheritProperties(KisForest< KoSvgTextContentElement >::depth_first_tail_iterator it)
KoSvgTextCharacterInfo infoFromCharacterResult(const CharacterResult &res, const int index)
#define KoSvgTextShape_SHAPEID
#define KoSvgTextShape_TEXTCONTOURGROUP
QSharedPointer< KoSvgTextShapeMemento > KoSvgTextShapeMementoSP
The HtmlSavingContext class provides context for saving a flake-based document to html.
KoXmlWriter & shapeWriter()
Provides access to the shape writer.
child_iterator childEnd()
Definition KisForest.h:880
child_iterator insert(child_iterator pos, X &&value)
Inserts element value into position pos. value becomes the child of the same parent as pos and is pla...
Definition KisForest.h:943
child_iterator move(child_iterator subtree, child_iterator newPos)
move a subtree to new position Moves subtree into a new position pointer by newPos....
Definition KisForest.h:994
A simple solid color shape background.
static bool IsCssWordSeparator(QString grapheme)
IsCssWordSeparator CSS has a number of characters it considers word-separators, which are used in jus...
QString stringProperty(const QString &name, const QString &defaultValue=QString()) const
bool property(const QString &name, QVariant &value) const
void addTemplate(const KoShapeTemplate &params)
void setToolTip(const QString &tooltip)
void setLoadingPriority(int priority)
void setIconName(const char *iconName)
void setXmlElementNames(const QString &nameSpace, const QStringList &elementNames)
QList< KoShape::ShapeChangeListener * > listeners
Definition KoShape_p.h:99
KoShapeContainer * parent
Definition KoShape_p.h:95
QScopedPointer< Private > d
Definition KoShape.h:974
virtual QPainterPath outline() const
Definition KoShape.cpp:559
KoShapeAnchor * anchor() const
virtual QVector< PaintOrder > paintOrder() const
paintOrder
Definition KoShape.cpp:693
static QVector< PaintOrder > defaultPaintOrder()
default paint order as per SVG specification
Definition KoShape.cpp:704
virtual void update() const
Definition KoShape.cpp:534
virtual void shapeChanged(ChangeType type, KoShape *shape=0)
Definition KoShape.cpp:1059
KoShapeContainer * parent() const
Definition KoShape.cpp:862
void shapeChangedPriv(KoShape::ChangeType type)
Definition KoShape.cpp:104
QTransform absoluteTransformation() const
Definition KoShape.cpp:335
void setTransformation(const QTransform &matrix)
Definition KoShape.cpp:374
ChangeType
Used by shapeChanged() to select which change was made.
Definition KoShape.h:92
@ RotationChanged
used after a setRotation()
Definition KoShape.h:94
@ StrokeChanged
the shapes stroke has changed
Definition KoShape.h:102
@ PositionChanged
used after a setPosition()
Definition KoShape.h:93
@ Deleted
the shape was deleted
Definition KoShape.h:101
@ ShearChanged
used after a shear()
Definition KoShape.h:96
@ ParentChanged
used after a setParent()
Definition KoShape.h:100
@ ContentChanged
the content of the shape changed e.g. a new image inside a pixmap/text change inside a textshape
Definition KoShape.h:106
@ ParameterChanged
the shapes parameter has changed (KoParameterShape only)
Definition KoShape.h:105
@ BackgroundChanged
the shapes background has changed
Definition KoShape.h:103
@ TextContourMarginChanged
used after text properties changed and modified the contour margin in the text shape
Definition KoShape.h:107
@ ScaleChanged
used after a scale()
Definition KoShape.h:95
@ SizeChanged
used after a setSize()
Definition KoShape.h:97
@ GenericMatrixChange
used after the matrix was changed without knowing which property explicitly changed
Definition KoShape.h:98
void scale(qreal sx, qreal sy)
Scale the shape using the zero-point which is the top-left corner.
Definition KoShape.cpp:209
void removeDependee(KoShape *shape)
Definition KoShape.cpp:1043
QTransform transformation() const
Returns the shapes local transformation matrix.
Definition KoShape.cpp:383
bool inheritPaintOrder() const
inheritPaintOrder
Definition KoShape.cpp:715
virtual void setPosition(const QPointF &position)
Set the position of the shape in pt.
Definition KoShape.cpp:267
virtual void updateAbsolute(const QRectF &rect) const
Definition KoShape.cpp:545
@ Stroke
Definition KoShape.h:117
void notifyChanged()
Definition KoShape.cpp:618
bool addDependee(KoShape *shape)
Definition KoShape.cpp:1026
void setShapeId(const QString &id)
Definition KoShape.cpp:885
void setInheritPaintOrder(bool value)
setInheritPaintOrder set inherit paint order.
Definition KoShape.cpp:710
QString name() const
Definition KoShape.cpp:955
bool isVisible(bool recursive=true) const
Definition KoShape.cpp:802
virtual void setSize(const QSizeF &size)
Resize the shape.
Definition KoShape.cpp:248
The KoSvgTextNodeIndex class.
KoShape * textPath()
textPath
KoSvgTextProperties * properties()
properties The properties for this node as a pointer.
QScopedPointer< Private > d
KoSvgText::TextOnPathInfo * textPathInfo()
textPathInfo the text path info for this node as a pointer.
@ TextAnchorId
KoSvgText::TextAnchor.
@ InlineSizeId
KoSvgText::AutoValue.
@ PaintOrder
QVector<KoShape::PaintOrder>
@ Opacity
Double, SVG shape opacity.
@ KraTextStyleType
string, used to identify the style preset type (character or paragraph).
@ TextCollapseId
KoSvgText::TextSpaceCollapse.
@ StrokeId
KoSvgText::StrokeProperty.
@ TextDecorationStyleId
KoSvgText::TextDecorationStyle.
@ FillId
KoSvgText::BackgroundProperty.
@ WritingModeId
KoSvgText::WritingMode.
@ DirectionId
KoSvgText::Direction.
@ TextWrapId
KoSvgText::TextWrap.
@ Visibility
Bool, CSS visibility.
@ TextDecorationLineId
Flags, KoSvgText::TextDecorations.
void removeProperty(PropertyId id)
QList< PropertyId > properties() const
static bool propertyIsBlockOnly(KoSvgTextProperties::PropertyId id)
QMap< QString, QString > convertToSvgTextAttributes() const
QVariant property(PropertyId id, const QVariant &defaultValue=QVariant()) const
QMap< QString, QString > convertParagraphProperties() const
convertParagraphProperties some properties only apply to the root shape, so we write those separately...
static const KoSvgTextProperties & defaultProperties()
bool hasProperty(PropertyId id) const
void setProperty(PropertyId id, const QVariant &value)
KoSvgText::CssLengthPercentage fontSize() const
KoSvgTextShapeFactory()
constructor
bool supports(const QDomElement &e, KoShapeLoadingContext &context) const override
Reimplemented.
KoShape * createDefaultShape(KoDocumentResourceManager *documentResources=0) const override
KoShape * createShape(const KoProperties *params, KoDocumentResourceManager *documentResources=0) const override
bool convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch)
upload the svg representation of text into the shape
QVector< LineBox > lineBoxes
QMap< int, int > logicalToVisualCursorPos
KisForest< KoSvgTextContentElement > textData
KoSvgTextShapeMementoImpl(const KisForest< KoSvgTextContentElement > &textData, const QVector< CharacterResult > &result, const QVector< LineBox > &lineBoxes, const QVector< CursorPos > &cursorPos, const QMap< int, int > &logicalToVisualCursorPos, const QString &plainText, const bool &isBidi, const QPointF &initialTextPosition)
QVector< CharacterResult > result
QVector< CursorPos > cursorPos
int posLeft(int pos, bool visual=false)
int posForPoint(QPointF point, int start=-1, int end=-1, bool *overlaps=nullptr)
posForPoint Finds the closest cursor position for the given point in shape coordinates.
KoShapeStrokeModelSP stroke() const override
QPair< int, int > findRangeForNodeIndex(const KoSvgTextNodeIndex &node) const
findRangeForNodeIndex Find the start and end cursor position for a given nodeIndex.
void setBackground(QSharedPointer< KoShapeBackground > background) override
int wordLeft(int pos, bool visual=false)
wordLeft return the cursorpos for the word left or the extreme of the line.
void notifyCursorPosChanged(int pos, int anchor)
Notify that the cursor position has changed.
void shapeChanged(ChangeType type, KoShape *shape) override
bool setCharacterTransformsOnRange(const int startPos, const int endPos, const QVector< QPointF > positions, const QVector< qreal > rotateDegrees, const bool deltaPosition=true)
setCharacterTransformsOnRange Set SVG 1.1 style character transforms on the given range....
void addTextPathAtEnd(KoShape *textPath)
addTextPathAtEnd add a textpath node at the end of the text.
bool relayoutIsBlocked() const
relayoutIsBlocked
QRectF endBulkAction() override
void setResolution(qreal xRes, qreal yRes) override
KoSvgTextShapeMementoSP getMemento()
Get a memento holding the current textdata and layout info.
void addShapeContours(QList< KoShape * > shapes, const bool inside=true)
addShapesContours Add shapes to the contours that make up the wrapping area.
bool fontMatchingDisabled() const
fontMatchingDisabled
QVector< CursorPos > cursorPos
QPainterPath selectionBoxes(int pos, int anchor)
selectionBoxes returns all selection boxes for a given range. Range will be normalized internally.
QVector< CharacterResult > result
void paint(QPainter &painter) const override
Paint the shape fill The class extending this one is responsible for painting itself....
QSharedPointer< KoShapeBackground > background() const override
KoSvgTextNodeIndex topLevelNodeForPos(int pos) const
topLevelNodeForPos Get, if possible, an index for the child element of the root at pos....
void removeTransformsFromRange(const int startPos, const int endPos)
bool saveHtml(HtmlSavingContext &context)
void setMemento(const KoSvgTextShapeMementoSP memento)
Set the text data and layout info, reset listening cursors to 0.
QRectF boundingRect() const override
Get the bounding box of the shape.
QList< QPainterPath > textWrappingAreas() const
textWrappingAreas The text wrapping areas are computed from shapesInside() and shapesSubtract(),...
int wordRight(int pos, bool visual=false)
wordRight return the cursorpos for the word right or the extreme of the line.
void setSize(const QSizeF &size) override
Resize the shape.
void notifyMarkupChanged()
Notify that the markup has changed.
KoSvgTextNodeIndex findNodeIndexForPropertyId(KoSvgTextProperties::PropertyId propertyId)
findNodeIndexForPropertyId
int indexForPos(int pos) const
indexForPos get the string index for a given cursor position.
int posUp(int pos, bool visual=false)
return position above.
KoShape * textOutline() const
textOutline This turns the text object into non-text KoShape(s) to the best of its abilities.
@ PreformattedText
Text-on-Path falls under this or PrePositionedText depending on collapse of lines.
@ TextInShape
Uses shape-inside to wrap and preserves spaces.
@ InlineWrap
Uses inline size to wrap and preserves spaces.
std::unique_ptr< KoSvgTextShape > copyRange(int index, int length) const
copyRange Copy the rich text for the given range.
bool setTextPathOnRange(KoShape *textPath, const int startPos=-1, const int endPos=-1)
setTextPathOnRange Set a text path on the specified range. In SVG text paths are always at the first ...
QList< KoShape * > shapesInside
static const QString & defaultPlaceholderText()
int nextLine(int pos)
nextLine get a position on the next line for this position.
QPointF initialTextPosition
void setPaintOrder(PaintOrder first, PaintOrder second) override
setPaintOrder set the paint order. As there's only three entries in any given paintorder,...
int previousLine(int pos)
previousLine get a position on the previous line for this position.
int posForPointLineSensitive(QPointF point)
posForPointLineSensitive When clicking on an empty space in a wrapped text, it is preferable to have ...
void leaveNodeSubtree()
Set the current node to its parent, leaving the subtree.
void setFontMatchingDisabled(const bool disable)
setDisableFontMatching
void cleanUp()
Cleans up the internal text data. Used by undo commands.
int lineStart(int pos)
lineStart return the 'line start' for this pos. This uses anchored chunks, so each absolute x,...
QPainterPath defaultCursorShape()
defaultCursorShape This returns a default cursor shape for when there's no text inside the text shape...
QVector< PaintOrder > paintOrder() const override
paintOrder
KoSvgTextProperties propertiesForPos(const int pos, bool inherited=false) const
Return the properties at a given position.
bool shapeInContours(KoShape *shape)
shapeInContours
int wordEnd(int pos)
wordEnd return the pos of the first wordbreak.
bool singleNode() const
singleNode Sometimes it is useful to know whether there's only a single text node for UX purposes.
int previousIndex(int pos)
previousIndex Return the first pos which has a lower string index.
void moveShapeInsideToIndex(KoShape *shapeInside, const int index)
moveShapeInsideToIndex Because the order of shapes inside shape-inside affects the text layout,...
void setShapesSubtract(QList< KoShape * > shapesSubtract)
setShapesSubtract
QPainterPath cursorForPos(int pos, QLineF &caret, QColor &color, double bidiFlagSize=1.0)
cursorForPos returns the QPainterPath associated with this cursorPosition.
int lineEnd(int pos)
lineEnd return the 'line end' for this pos. This uses anchored chunks, so each absolute x,...
void enterNodeSubtree()
Set the current node to its first child, entering the subtree.
void setPropertiesAtPos(int pos, KoSvgTextProperties properties)
setPropertiesAtPos will set the properties at pos.
int posDown(int pos, bool visual=false)
return position below.
KoShapeManager * internalShapeManager() const
internalShapeManager
void debugParsing()
Outputs debug with the current textData tree.
QList< KoSvgTextProperties > propertiesForRange(const int startPos, const int endPos, bool inherited=false) const
propertiesForRange get the properties for a range.
TextType textType() const
textType This enum gives an indication of what kind of text this shape is. The different text types a...
int posForIndex(int index, bool firstIndex=false, bool skipSynthetic=false) const
posForIndex Get the cursor position for a given index in a string.
KoSvgTextNodeIndex nodeForTextPath(KoShape *textPath) const
nodeForTextPath TextPaths are set on toplevel content elements. This function allows for searching wh...
int nextPos(int pos, bool visual)
nextPos get the next position.
void mergePropertiesIntoRange(const int startPos, const int endPos, const KoSvgTextProperties properties, const QSet< KoSvgTextProperties::PropertyId > removeProperties=QSet< KoSvgTextProperties::PropertyId >())
mergePropertiesIntoRange Merge given properties into the given range. This will first split the nodes...
QList< KoSvgTextCharacterInfo > getPositionsAndRotationsForRange(const int startPos, const int endPos) const
getPositionsAndRotationsForRange
void setCharacterTransformsFromLayout()
setCharacterTransformsFromLayout Converts the text to a prepositioned SVG 1.1 text....
void setShapesInside(QList< KoShape * > shapesInside)
setShapesInside
QScopedPointer< Private > d
int wordStart(int pos)
wordStart return the first pos before a wordbreak in the start direction.
QPainterPath outline() const override
QList< KoShape * > textPathsAtRange(const int startPos=-1, const int endPos=-1)
textPathsAtRange Get a list of textPaths at the given range. This includes textPaths whose node is on...
void setMementoImpl(const KoSvgTextShapeMementoSP memento)
void startBulkAction() override
void paintStroke(QPainter &painter) const override
paintStroke paints the shape's stroked outline
bool saveSvg(SvgSavingContext &context) override
Saves SVG data.
int previousPos(int pos, bool visual)
previousPos get the previous position.
QMap< QString, QString > shapeTypeSpecificStyles(SvgSavingContext &context) const
int posRight(int pos, bool visual=false)
QList< KoShape * > shapesSubtract
void setRelayoutBlocked(const bool disable)
void setStroke(KoShapeStrokeModelSP stroke) override
KoShape * cloneShape() const override
creates a deep copy of the shape or shape's subtree
bool insertRichText(int pos, const KoSvgTextShape *richText, bool inheritPropertiesIfPossible=false)
insertRichText Insert rich text at the given cursor pos. This will first split contents at the given ...
QSizeF size() const override
Get the size of the shape in pt.
QList< KoShape * > textPaths
QPainterPath underlines(int pos, int anchor, KoSvgText::TextDecorations decor, KoSvgText::TextDecorationStyle style, qreal minimum, bool thick)
void paintDebug(QPainter &painter, DebugElements elements) const
bool insertText(int pos, QString text)
insertText Insert a text somewhere in the KoTextShape.
bool removeText(int &index, int &length)
removeText Where insert text explicitly uses a cursorposition, remove text uses a string index....
void convertCharTransformsToPreformatted(bool makeInlineSize=false)
convertCharTransformsToPreformatted Converts the text to a preformatted SVG 2.0 text....
void removeShapesFromContours(QList< KoShape * > shapes, bool callUpdate=true, bool cleanup=true)
removeShapesFromContours Remove list of shapes from any of the internal lists.
KoSvgText::WritingMode writingMode() const
writingMode There's a number of places we need to check the writing mode to provide proper controls.
~KoSvgTextShape() override
static QList< QPainterPath > generateTextAreas(const QList< KoShape * > shapesInside, const QList< KoShape * > shapesSubtract, const KoSvgTextProperties &props)
generateTextAreas Generates text areas with the given shapes and properties. This is used to paint pr...
void relayout() const
QRectF outlineRect() const override
int nextIndex(int pos)
nextIndex Return the first cursor position with a higher string index.
KoSvgTextProperties textProperties() const
static const QString svg
Definition KoXmlNS.h:39
void startElement(const char *tagName, bool indentInside=true)
void addTextNode(const QString &str)
void endElement()
void addAttribute(const char *attrName, const QString &value)
Definition KoXmlWriter.h:61
Context for saving svg files.
QString createUID(const QString &base)
Create a unique id from the specified base text.
QScopedPointer< KoXmlWriter > shapeWriter
QString getID(const KoShape *obj)
Returns the unique id for the given shape.
static void saveSvgFill(QSharedPointer< KoShapeBackground > background, const bool fillRuleEvenOdd, const QRectF outlineRect, const QSizeF size, const QTransform absoluteTransform, SvgSavingContext &context)
Saves fill style of specified shape.
static QString embedShape(const KoShape *shape, SvgSavingContext &context)
static void saveSvgBasicStyle(const bool isVisible, const qreal transparency, const QVector< KoShape::PaintOrder > paintOrder, bool inheritPaintorder, SvgSavingContext &context, bool textShape=false)
Saves only stroke, fill and transparency of the shape.
static void saveSvgStyle(KoShape *shape, SvgSavingContext &context)
Saves the style of the specified shape.
static void saveMetadata(const KoShape *shape, SvgSavingContext &context)
static void saveSvgStroke(KoShapeStrokeModelSP, SvgSavingContext &context)
Saves stroke style of specified shape.
static void writeTransformAttributeLazy(const QString &name, const QTransform &transform, KoXmlWriter &shapeWriter)
Writes a transform as an attribute name iff the transform is not empty.
Definition SvgUtil.cpp:124
Implements exporting shapes to SVG.
Definition SvgWriter.h:33
bool saveDetached(QIODevice &outputDevice)
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
bool isWhiteSpace(char c)
qreal kisDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:190
T kisRadiansToDegrees(T radians)
Definition kis_global.h:181
T kisDegreesToRadians(T degrees)
Definition kis_global.h:176
#define koIconNameCStr(name)
Definition kis_icon.h:28
#define koIconName(name)
Definition kis_icon.h:27
ResultIterator hierarchyBegin(Iterator it)
Definition KisForest.h:419
ChildIterator< value_type, is_const > childBegin(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:290
ResultIterator hierarchyEnd(Iterator it)
Definition KisForest.h:427
ChildIterator< value_type, is_const > siblingCurrent(ChildIterator< value_type, is_const > it)
Definition KisForest.h:240
int size(const Forest< T > &forest)
Definition KisForest.h:1232
ChildIterator< value_type, is_const > siblingEnd(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:255
ChildIterator< value_type, is_const > childEnd(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:300
int depth(typename Forest< T >::const_child_iterator beginIt, typename Forest< T >::const_child_iterator endIt)
Definition KisForest.h:1213
KRITAFLAKE_EXPORT void resizeShapeCommon(KoShape *shape, qreal scaleX, qreal scaleY, const QPointF &absoluteStillPoint, bool useGlobalMode, bool usePostScaling, const QTransform &postScalingCoveringTransform)
Definition KoFlake.cpp:313
TextAnchor
Where the text is anchored for SVG 1.1 text and 'inline-size'.
Definition KoSvgText.h:79
@ AnchorEnd
Anchor right for LTR, left for RTL.
Definition KoSvgText.h:82
@ AnchorMiddle
Anchor to the middle.
Definition KoSvgText.h:81
TextDecorationStyle
Style of the text-decoration.
Definition KoSvgText.h:265
@ Solid
Draw a solid line.Ex: --—.
Definition KoSvgText.h:266
@ Dashed
Draw a dashed line. Ex: - - - - -.
Definition KoSvgText.h:269
@ Dotted
Draw a dotted line. Ex: .....
Definition KoSvgText.h:268
@ DecorationOverline
Definition KoSvgText.h:260
@ DecorationLineThrough
Definition KoSvgText.h:261
@ DecorationUnderline
Definition KoSvgText.h:259
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionRightToLeft
Definition KoSvgText.h:50
@ HorizontalTB
Definition KoSvgText.h:38
@ RenderingOptimizeSpeed
Definition KoSvgText.h:316
TextSpaceCollapse
Definition KoSvgText.h:96
@ Collapse
Collapse white space sequences into a single character.
Definition KoSvgText.h:97
@ PreserveSpaces
required for 'xml:space="preserve"' emulation.
Definition KoSvgText.h:102
QPointF finalPosition
the final position, taking into account both CSS and SVG positioning considerations.
KoSvgText::FontMetrics metrics
Fontmetrics for current font, in Freetype scanline coordinates.
bool anchored_chunk
whether this is the start of a new chunk.
QPointF textPathAndAnchoringOffset
Offset caused by textPath and anchoring.
QPointF cssPosition
the position in accordance with the CSS specs, as opossed to the SVG spec.
QRectF layoutBox() const
layoutBox
QPointF textLengthOffset
offset caused by textLength
QTransform finalTransform() const
QLineF caret
Caret for this characterResult.
bool rtl
Whether the current glyph is right-to-left, as opposed to the markup.
QColor color
Which color the current position has.
QVector< QPointF > offsets
The advance offsets for each grapheme index.
int cluster
Which character result this position belongs in.
int offset
Which offset this position belongs with.
qreal bottom
Bottom inset.
Definition KoInsets.h:50
qreal right
Right inset.
Definition KoInsets.h:52
qreal top
Top inset.
Definition KoInsets.h:49
qreal left
Left inset.
Definition KoInsets.h:51
QString iconName
Icon name.
QString toolTip
The tooltip text for the template.
QString name
The name to be shown for this template.
The KoSvgTextCharacterInfo class This is a small struct to convey information about character positio...
KoSvgText::FontMetrics metrics
<Whether the character is in the middle of a cluster.
The KoSvgTextContentElement struct.
KoSvgTextProperties properties
The textProperties. This includes.
QString textPathId
The textpath's name, if any.
QString text
Plain text of the current node. Use insertText and removeText to manipulate it.
KisForest< KoSvgTextContentElement >::child_iterator textElement
Private(KisForest< KoSvgTextContentElement >::child_iterator _textElement, KoShape *_textPath)
ShapeChangeListener so we can inform any text cursors that the cursor needs updating.
void notifyShapeChanged(ChangeType type, KoShape *shape) override
virtual void notifyCursorPosChanged(int pos, int anchor)=0
BackgroundProperty is a special wrapper around KoShapeBackground for managing it in KoSvgTextProperti...
Definition KoSvgText.h:714
boost::optional< qreal > yPos
Definition KoSvgText.h:606
boost::optional< qreal > dxPos
Definition KoSvgText.h:607
boost::optional< qreal > dyPos
Definition KoSvgText.h:608
boost::optional< qreal > rotate
Definition KoSvgText.h:609
boost::optional< qreal > xPos
Definition KoSvgText.h:605
qint32 underlineThickness
underline thickness from font.
Definition KoSvgText.h:357
StrokeProperty is a special wrapper around KoShapeStrokeModel for managing it in KoSvgTextProperties.
Definition KoSvgText.h:733
The LineBox struct.
QPointF baselineTop
Used to identify the top of the line for baseline-alignment.
QVector< LineChunk > chunks
QPointF baselineBottom
Used to identify the bottom of the line for baseline-alignment.
QVector< int > chunkIndices
charResult indices that belong to this chunk.
QLineF length
Used to measure how long the current line is allowed to be.