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 {
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 {
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) {
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) {
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->getCursorPos(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->getCursorPos(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->getCursorPos(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 }
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 }
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->getCursorPos(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 const int currentIndex = d->getCursorPos(pos).index;
410
411 for (int i = pos; i >= 0; i--) {
412 if (d->getCursorPos(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->getCursorPos(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->getCursorPos(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 // Also test the next cluster on whether it is a hardbreak. This is because hardbreaks by default get hidden.
581 if (i < d->cursorPos.size()-1 && i > pos) {
582 const CursorPos nextCursorPos = d->cursorPos.at(i+1);
583 bool found = false;
584 for (int j = cursorPos.cluster; j < nextCursorPos.cluster; j++) {
585 if (d->result.at(j).breakType == BreakType::HardBreak && d->result.at(j).addressable) {
586 wordEnd = qMax(pos, wordEnd);
587 found = true;
588 break;
589 }
590 }
591 if (found) {
592 break;
593 }
594 }
595 }
596
597 return wordEnd;
598}
599
601{
602 if (pos <= 0 || pos > d->cursorPos.size() || d->result.isEmpty() || d->cursorPos.isEmpty()) {
603 return pos;
604 }
605
606 int wordStart = pos;
607 for (int i = pos; i >= 0; i--) {
608 wordStart = i;
609 CursorPos cursorPos = d->getCursorPos(i);
610 const CharacterResult res = d->result.at(cursorPos.cluster);
612 && (cursorPos.offset == 0 || cursorPos.offset+1 == res.cursorInfo.offsets.size())
613 && i < pos) {
614 break;
615 }
616 // Also test the previous cluster on whether it is a hardbreak. This is because hardbreaks by default get hidden.
617 if (i > 1 && i < pos) {
618 const CursorPos prevCursorPos = d->getCursorPos(i-1);
619 bool found = false;
620 for (int j = cursorPos.cluster; j > prevCursorPos.cluster; j--) {
621 if (d->result.at(j).breakType == BreakType::HardBreak && d->result.at(j).addressable) {
622 wordStart = qMin(pos, i-1);
623 found = true;
624 break;
625 }
626 }
627 if (found) {
628 break;
629 }
630 }
631 }
632
633 return wordStart;
634}
635
637{
639 double fontSize = this->textProperties().fontSize().value;
640 QPainterPath p;
641 if (mode == KoSvgText::HorizontalTB) {
642 p.moveTo(0, fontSize*0.2);
643 p.lineTo(0, -fontSize);
644 } else {
645 p.moveTo(-fontSize * 0.5, 0);
646 p.lineTo(fontSize, 0);
647 }
648 p.translate(d->initialTextPosition);
649
650 return p;
651}
652
653QPainterPath KoSvgTextShape::cursorForPos(int pos, QLineF &caret, QColor &color, double bidiFlagSize)
654{
655 if (d->result.isEmpty() || d->cursorPos.isEmpty() || pos < 0 || pos >= d->cursorPos.size()) {
656 return defaultCursorShape();
657 }
658 QPainterPath p;
659
660 CursorPos cursorPos = d->getCursorPos(pos);
661
662 CharacterResult res = d->result.at(cursorPos.cluster);
663
664 const QTransform tf = res.finalTransform();
665 color = res.cursorInfo.color;
666 caret = res.cursorInfo.caret;
667 caret.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
668
669 p.moveTo(tf.map(caret.p1()));
670 p.lineTo(tf.map(caret.p2()));
671 if (d->isBidi && bidiFlagSize > 0) {
672 int sign = res.cursorInfo.rtl ? -1 : 1;
673 double bidiFlagHalf = bidiFlagSize * 0.5;
674 QPointF point3;
675 QPointF point4;
677 qreal slope = bidiFlagHalf * (caret.dx()/ caret.dy());
678 point3 = QPointF(caret.p2().x() + slope + (sign * bidiFlagHalf), caret.p2().y() + bidiFlagHalf);
679 point4 = QPointF(point3.x() + slope - (sign * bidiFlagHalf),point3.y() + bidiFlagHalf);
680 } else {
681 qreal slope = bidiFlagHalf * (caret.dy()/ caret.dx());
682 point3 = QPointF(caret.p2().x() - bidiFlagHalf, caret.p2().y() - slope + (sign * bidiFlagHalf));
683 point4 = QPointF(point3.x() - bidiFlagHalf, point3.y() - slope - (sign * bidiFlagHalf));
684 }
685 p.lineTo(tf.map(point3));
686 p.lineTo(tf.map(point4));
687 }
688 caret = tf.map(caret);
689
690 return p;
691}
692
693QPainterPath KoSvgTextShape::selectionBoxes(int pos, int anchor)
694{
695 int start = qMin(pos, anchor);
696 int end = qMax(pos, anchor);
697 end = qMin(d->cursorPos.size()-1, end);
698
699 if (start == end || start < 0) {
700 return QPainterPath();
701 }
702
703 QPainterPath p;
704 p.setFillRule(Qt::WindingFill);
705 for (int i = start+1; i <= end; i++) {
706 CursorPos cursorPos = d->getCursorPos(i);
707 CharacterResult res = d->result.at(cursorPos.cluster);
708 const QTransform tf = res.finalTransform();
709 QLineF first = res.cursorInfo.caret;
710 QLineF last = first;
711 if (res.cursorInfo.rtl) {
712 last.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, res.advance));
713 first.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
714 } else {
715 first.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, QPointF()));
716 last.translate(res.cursorInfo.offsets.value(cursorPos.offset, res.advance));
717 }
718 QPolygonF poly;
719 poly << first.p1() << first.p2() << last.p2() << last.p1() << first.p1();
720 p.addPolygon(tf.map(poly));
721 }
722
723 return p;
724}
725
726QPainterPath KoSvgTextShape::underlines(int pos, int anchor, KoSvgText::TextDecorations decor, KoSvgText::TextDecorationStyle style, qreal minimum, bool thick)
727{
728 int start = qMin(pos, anchor);
729 int end = qMax(pos, anchor);
730
731 if (start == end || start < 0 || end >= d->cursorPos.size()) {
732 return QPainterPath();
733 }
734 QPainterPathStroker stroker;
735
737 stroker.setCapStyle(Qt::FlatCap);
738 if (style == KoSvgText::Solid) {
739 stroker.setDashPattern(Qt::SolidLine);
740 } else if (style == KoSvgText::Dashed) {
741 stroker.setDashPattern(Qt::DashLine);
742 } else if (style == KoSvgText::Dotted) {
743 stroker.setDashPattern(Qt::DotLine);
744 } else {
745 stroker.setDashPattern(Qt::SolidLine);
746 }
747
748 QPainterPath underPath;
749 QPainterPath overPath;
750 QPainterPath middlePath;
751 qint32 strokeWidth = 0;
752 QPointF inset = mode == KoSvgText::HorizontalTB? QPointF(minimum*0.5, 0): QPointF(0, minimum*0.5);
753 for (int i = start+1; i <= end; i++) {
754 CursorPos pos = d->getCursorPos(i);
755 CharacterResult res = d->result.at(pos.cluster);
756 strokeWidth += res.metrics.underlineThickness;
757 const QTransform tf = res.finalTransform();
758 QPointF first = res.cursorInfo.caret.p1();
759 QPointF last = first;
760 if (res.cursorInfo.rtl) {
761 last += res.cursorInfo.offsets.value(pos.offset-1, res.advance);
762 first += res.cursorInfo.offsets.value(pos.offset, QPointF());
763 if (i == start+1) {
764 first -= inset;
765 }
766 if (i == end) {
767 last += inset;
768 }
769 } else {
770 first += res.cursorInfo.offsets.value(pos.offset-1, QPointF());
771 last += res.cursorInfo.offsets.value(pos.offset, res.advance);
772 if (i == start+1) {
773 first += inset;
774 }
775 if (i == end) {
776 last -= inset;
777 }
778 }
779
780 if (decor.testFlag(KoSvgText::DecorationUnderline)){
781 underPath.moveTo(tf.map(first));
782 underPath.lineTo(tf.map(last));
783 }
784 QPointF diff = res.cursorInfo.caret.p2() - res.cursorInfo.caret.p1();
785 if (decor.testFlag(KoSvgText::DecorationOverline)){
786 overPath.moveTo(tf.map(first+diff));
787 overPath.lineTo(tf.map(last+diff));
788 }
789 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
790 middlePath.moveTo(tf.map(first+(diff*0.5)));
791 middlePath.lineTo(tf.map(last+(diff*0.5)));
792 }
793 }
794
795 const qreal freetypePixelsToPt = (1.0 / 64.0) * (72. / qMin(d->xRes, d->yRes));
796 const qreal width = strokeWidth > 0 ? qMax(qreal(strokeWidth/qMax(1, end-(start+1)))*freetypePixelsToPt, minimum): minimum;
797
798 stroker.setWidth(thick? width*2: width);
799
800 QPainterPath final;
801 if (decor.testFlag(KoSvgText::DecorationUnderline)){
802 final.addPath(stroker.createStroke(underPath));
803 }
804 if (decor.testFlag(KoSvgText::DecorationOverline)){
805 final.addPath(stroker.createStroke(overPath));
806 }
807 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
808 final.addPath(stroker.createStroke(middlePath));
809 }
810
811 return final;
812}
813
814int KoSvgTextShape::posForPoint(QPointF point, int start, int end, bool *overlaps)
815{
816 int a = 0;
817 int b = d->cursorPos.size();
818 if (start >= 0 && end >= 0) {
819 a = qMax(start, a);
820 b = qMin(end, b);
821 }
822 double closest = std::numeric_limits<double>::max();
823 int candidate = 0;
824 for (int i = a; i < b; i++) {
825 CursorPos pos = d->getCursorPos(i);
826 CharacterResult res = d->result.at(pos.cluster);
827 QPointF cursorStart = res.finalPosition;
828 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
829 double distance = kisDistance(cursorStart, point);
830 if (distance < closest) {
831 candidate = i;
832 closest = distance;
833 if (overlaps) {
834 *overlaps = res.finalTransform().map(res.layoutBox()).containsPoint(point, Qt::WindingFill);
835 }
836 }
837 }
838 return candidate;
839}
840
842{
843 bool overlaps = false;
844 int initialPos = posForPoint(point, -1, -1, &overlaps);
845
846 if (overlaps) {
847 return initialPos;
848 }
849
851
852 int candidateLineStart = 0;
853 double closest = std::numeric_limits<double>::max();
854 for (int i = 0; i < d->cursorPos.size(); i++) {
855 CursorPos pos = d->cursorPos.at(i);
856 CharacterResult res = d->result.at(pos.cluster);
857 if (res.anchored_chunk) {
858 QLineF caret = res.cursorInfo.caret;
859 caret.translate(res.finalPosition);
860 QPointF cursorStart = res.finalPosition;
861 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
862 double distance = kisDistance(cursorStart, point);
863 if (mode == KoSvgText::HorizontalTB) {
864 if (caret.p1().y() > point.y() && caret.p2().y() <= point.y() && closest > distance) {
865 candidateLineStart = i;
866 closest = distance;
867 }
868 } else {
869 if (caret.p2().x() > point.x() && caret.p1().x() <= point.x() && closest > distance) {
870 candidateLineStart = i;
871 closest = distance;
872 }
873 }
874 }
875 }
876
877 if (candidateLineStart > -1) {
878 int end = lineEnd(candidateLineStart);
879 initialPos = posForPoint(point, candidateLineStart, qMin(end + 1, d->cursorPos.size()));
880 }
881
882 return initialPos;
883}
884
885int KoSvgTextShape::posForIndex(int index, bool firstIndex, bool skipSynthetic) const
886{
887 int pos = -1;
888 if (d->cursorPos.isEmpty() || index < 0) {
889 return pos;
890 }
891 for (int i = 0; i< d->cursorPos.size(); i++) {
892 if (skipSynthetic && d->cursorPos.at(i).synthetic) {
893 continue;
894 }
895 if (d->cursorPos.at(i).index <= index) {
896 pos = i;
897 if (d->cursorPos.at(i).index == index && firstIndex) {
898 break;
899 }
900 } else if (d->cursorPos.at(i).index > index) {
901 break;
902 }
903 }
904
905 return pos;
906}
907
909{
910 if (d->cursorPos.isEmpty() || pos < 0) {
911 return -1;
912 }
913
914 return d->getCursorPos(pos).index;
915}
916
918{
919 return d->initialTextPosition;
920}
921
922bool KoSvgTextShape::insertText(int pos, QString text)
923{
924 bool success = false;
925 int currentIndex = 0;
926
930 int elementIndex = 0;
931 int insertionIndex = 0;
932 if (pos > -1 && !d->cursorPos.isEmpty()) {
933 CursorPos cursorPos = d->getCursorPos(pos);
934 CharacterResult res = d->result.at(cursorPos.cluster);
935 elementIndex = res.plaintTextIndex;
936 insertionIndex = cursorPos.index;
937 elementIndex = qMin(elementIndex, d->result.size()-1);
938 }
939 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
940 if (it != d->textData.depthFirstTailEnd()) {
941 const int offset = insertionIndex - currentIndex;
942 it->insertText(offset, text);
943
944 d->insertTransforms(d->textData, insertionIndex, text.size(), (elementIndex == insertionIndex));
947 success = true;
948 }
949 return success;
950}
951
952bool KoSvgTextShape::removeText(int &index, int &length)
953{
954 bool success = false;
955 if (index < -1) {
956 return success;
957 }
958 int currentLength = length;
959 int endLength = 0;
960 while (currentLength > 0) {
961 int currentIndex = 0;
962
963 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, index, true);
964 if (it != d->textData.depthFirstTailEnd()) {
965 int offset = index > currentIndex? index - currentIndex: 0;
966 int size = it->numChars(false);
967 it->removeText(offset, currentLength);
968 int diff = size - it->numChars(false);
969 currentLength -= diff;
970 endLength += diff;
971
972 if (index >= currentIndex) {
973 index = currentIndex + offset;
974 }
975
976 d->removeTransforms(d->textData, index, endLength);
977
978 success = true;
979 } else {
980 currentLength = -1;
981 }
982 }
983 if (success) {
984 length = endLength;
987 }
988 return success;
989}
990
991KoSvgTextProperties KoSvgTextShape::propertiesForPos(const int pos, bool inherited) const
992{
993 return propertiesForRange(pos, pos, inherited).value(0, KoSvgTextProperties());
994}
995
998 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it));
999 parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) {
1000 hierarchy.append(parentIt);
1001 }
1003 while (!hierarchy.isEmpty()) {
1004 auto it = hierarchy.takeLast();
1005 KoSvgTextProperties p = it->properties;
1006 p.inheritFrom(props, true);
1007 props = p;
1008 }
1009 return props;
1010}
1011
1012QList<KoSvgTextProperties> KoSvgTextShape::propertiesForRange(const int startPos, const int endPos, bool inherited) const
1013{
1015
1017 if (d->isLoading) return props;
1018
1019 if (((startPos < 0 || startPos >= d->cursorPos.size()) && startPos == endPos) || d->cursorPos.isEmpty()) {
1020 props = {KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties()};
1021 return props;
1022 }
1023 const int startIndex = d->getCursorPos(startPos).index;
1024 const int endIndex = d->getCursorPos(endPos).index;
1025 int sought = startIndex;
1026 if (startIndex == endIndex) {
1027 int currentIndex = 0;
1028 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1029 if (it != d->textData.depthFirstTailEnd()) {
1030 if (inherited) {
1031 props.append(inheritProperties(it));
1032 } else {
1033 props.append(it->properties);
1034 }
1035 } else {
1036 currentIndex = 0;
1037 it = d->findTextContentElementForIndex(d->textData, currentIndex, sought - 1);
1038 if (it != d->textData.depthFirstTailEnd()) {
1039 if (inherited) {
1040 props.append(inheritProperties(it));
1041 } else {
1042 props.append(it->properties);
1043 }
1044 }
1045 }
1046 } else {
1047 while(sought < endIndex) {
1048 int currentIndex = 0;
1049 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1050 if (KisForestDetail::siblingCurrent(it) == d->textData.childBegin()) {
1051 // If there's a selection and the search algorithm only returns the root, return empty.
1052 // The root text properties should be retrieved explicitly (either by using -1 as pos, or by calling textProperties()).
1053 props = {KoSvgTextProperties()};
1054 return props;
1055 } else if (it != d->textData.depthFirstTailEnd()) {
1056 if (inherited) {
1057 props.append(inheritProperties(it));
1058 } else {
1059 props.append(it->properties);
1060 }
1061 }
1062 sought = currentIndex + it->numChars(false);
1063 }
1064 }
1065
1066 return props;
1067}
1068
1070{
1071 if (pos < 0 || d->cursorPos.isEmpty()) {
1072 if (KisForestDetail::size(d->textData)) {
1073 d->textData.childBegin()->properties = properties;
1074 }
1075 notifyChanged();
1077 return;
1078 }
1079 CursorPos cursorPos = d->getCursorPos(pos);
1080 CharacterResult res = d->result.at(cursorPos.cluster);
1081 int currentIndex = 0;
1082 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, res.plaintTextIndex);
1083 if (it != d->textData.depthFirstTailEnd()) {
1084 it->properties = properties;
1085 notifyChanged();
1087 }
1088}
1089
1091 const int endPos,
1092 const KoSvgTextProperties properties,
1093 const QSet<KoSvgTextProperties::PropertyId> removeProperties)
1094{
1095 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1096 if (KisForestDetail::size(d->textData)) {
1097 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1098 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1099 }
1100 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1101 d->textData.childBegin()->properties.removeProperty(p);
1102 }
1103 }
1104 notifyChanged();
1107 || removeProperties.contains(KoSvgTextProperties::ShapePaddingId)
1108 || removeProperties.contains(KoSvgTextProperties::ShapeMarginId)) {
1110 } else {
1112 }
1113 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1115 }
1118 }
1119 return;
1120 }
1121 const int startIndex =d->getCursorPos(startPos).index;
1122 const int endIndex = d->getCursorPos(endPos).index;
1123 if (startIndex != endIndex) {
1124 KoSvgTextShape::Private::splitContentElement(d->textData, startIndex);
1125 KoSvgTextShape::Private::splitContentElement(d->textData, endIndex);
1126 }
1127 bool changed = false;
1128 int currentIndex = 0;
1129 KoSvgText::AutoValue inlineSize = d->textData.childBegin()->properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
1130 bool isWrapping = !d->shapesInside.isEmpty() || !inlineSize.isAuto;
1131 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1132 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0) {
1133 continue;
1134 }
1135
1136 if (currentIndex >= startIndex && currentIndex < endIndex) {
1137 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1139 d->textData.childBegin()->properties.removeProperty(p);
1140 } else {
1141 it->properties.removeProperty(p);
1142 }
1143 }
1144 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1146 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1147 } else {
1148 it->properties.setProperty(p, properties.property(p));
1149 }
1150 }
1151
1152 changed = true;
1153 }
1154 currentIndex += it->numChars(false);
1155 }
1156
1157 if (changed){
1158 KoSvgTextShape::Private::cleanUp(d->textData);
1159 notifyChanged();
1161 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1163 }
1166 }
1167 }
1168}
1169
1170std::unique_ptr<KoSvgTextShape> KoSvgTextShape::copyRange(int index, int length) const
1171{
1172 KoSvgTextShape *clone = new KoSvgTextShape(*this);
1173 int zero = 0;
1174 int endRange = index + length;
1175 int size = KoSvgTextShape::Private::numChars(clone->d->textData.childBegin(), false) - endRange;
1176 clone->removeText(endRange, size);
1177 clone->removeText(zero, index);
1178 KoSvgTextShape::Private::cleanUp(clone->d->textData);
1179 return std::unique_ptr<KoSvgTextShape>(clone);
1180}
1181
1182bool KoSvgTextShape::insertRichText(int pos, const KoSvgTextShape *richText, bool inheritPropertiesIfPossible)
1183{
1184 bool success = false;
1185 int currentIndex = 0;
1186 int elementIndex = 0;
1187 int insertionIndex = 0;
1188
1189 if (isEnd(richText->d->textData.childBegin())) {
1190 // rich text is empty.
1191 return success;
1192 }
1193
1194 if (pos > -1 && !d->cursorPos.isEmpty()) {
1195 CursorPos cursorPos = d->getCursorPos(pos);
1196 CharacterResult res = d->result.at(cursorPos.cluster);
1197 elementIndex = res.plaintTextIndex;
1198 insertionIndex = cursorPos.index;
1199 elementIndex = qMin(elementIndex, d->result.size()-1);
1200 }
1201
1202 KoSvgTextShape::Private::splitContentElement(this->d->textData, insertionIndex);
1203
1204 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, insertionIndex);
1205 auto richTextIt = d->textData.childEnd();
1206 if (it != d->textData.depthFirstTailEnd()) {
1207 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingCurrent(it));
1208 success = true;
1209 } else {
1210 currentIndex = 0;
1211 it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
1212 if (it != d->textData.depthFirstTailEnd()) {
1213 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingEnd(siblingCurrent(it)));
1214 success = true;
1215 }
1216 }
1217
1218 if (richTextIt != d->textData.childEnd()) {
1219 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1221 richTextIt->properties.removeProperty(p);
1222 }
1223 }
1224 auto parentIt = KisForestDetail::hierarchyBegin(richTextIt);
1225 auto parentEnd = KisForestDetail::hierarchyEnd(richTextIt);
1226 if (parentIt != parentEnd) {
1227 parentIt++;
1228 if (inheritPropertiesIfPossible && parentIt != parentEnd) {
1229 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1230 if (richTextIt->properties.inheritsProperty(p, parentIt->properties)) {
1231 richTextIt->properties.removeProperty(p);
1232 }
1233 }
1234 }
1235 }
1236 }
1237
1238 if (success) {
1239 notifyChanged();
1241 }
1242 return success;
1243}
1244
1246{
1247 KoSvgTextShape::Private::cleanUp(d->textData);
1248 notifyChanged();
1250}
1251
1252bool KoSvgTextShape::setCharacterTransformsOnRange(const int startPos, const int endPos, const QVector<QPointF> positions, const QVector<qreal> rotateDegrees, const bool deltaPosition)
1253{
1254 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1255 return false;
1256 }
1257 const int startIndex = d->getCursorPos(qMin(startPos, endPos)).index;
1258 int endIndex = d->getCursorPos(qMax(startPos, endPos)).index;
1259 if (startIndex == endIndex) {
1260 endIndex += 1;
1261 while(d->result.at(endIndex).middle) {
1262 endIndex += 1;
1263 if (endIndex > d->result.size()) break;
1264 }
1265 }
1266
1267 bool changed = false;
1268
1269 QVector<KoSvgText::CharTransformation> resolvedTransforms = Private::resolvedTransformsForTree(d->textData, !shapesInside().isEmpty(), true);
1270 Private::removeTransforms(d->textData, startIndex, endIndex-startIndex);
1271 QPointF totalStartDelta;
1272 QPointF anchorAbsolute;
1273 QPointF anchorCssPos;
1274
1275 const KoSvgText::Direction dir = KoSvgText::Direction(this->textProperties().propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
1277
1278 const CharacterResult startRes = d->result.value(startIndex);
1279 const QPointF startAdvance = startRes.cursorInfo.rtl? startRes.advance: QPointF();
1280
1281 if (deltaPosition) {
1282 for (int i = 0; i< startIndex; i++) {
1283 KoSvgText::CharTransformation tf = resolvedTransforms.value(i);
1284 if (tf.xPos) {
1285 totalStartDelta.setX(0);
1286 anchorAbsolute.setX(*tf.xPos);
1287 }
1288 if (tf.yPos) {
1289 totalStartDelta.setY(0);
1290 anchorAbsolute.setY(*tf.yPos);
1291 }
1292 if (tf.startsNewChunk()) {
1293 const CharacterResult res = d->result.value(i);
1294 anchorCssPos = res.cssPosition;
1295 }
1296 totalStartDelta += tf.relativeOffset();
1297 }
1298 } else {
1299 bool rtl = (dir == KoSvgText::DirectionRightToLeft);
1300 QPointF positionAtVisualEnd = (rtl? d->result.first(): d->result.last()).finalPosition;
1302 anchorAbsolute = positionAtVisualEnd;
1303 } else if (anchor == KoSvgText::AnchorMiddle) {
1304 anchorAbsolute = positionAtVisualEnd/2;
1305 }
1306 }
1307
1308 int currentIndex = 0;
1309 QPointF accumulatedOffset;
1310 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1311 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0 || !it->textPathId.isEmpty()) {
1312 continue;
1313 }
1314
1315 int endContentElement = it->finalResultIndex;
1316
1317 if (endContentElement >= startIndex && currentIndex <= endIndex) {
1318
1320 int addressableOffset = 0;
1321 for (int i = currentIndex; (i < endContentElement); i++) {
1322 const CharacterResult res = d->result.value(i);
1323 if (!res.addressable) {
1324 addressableOffset += 1;
1325 continue;
1326 }
1327
1328 const int transformIndex = (i - startIndex) - addressableOffset;
1329 KoSvgText::CharTransformation tf = resolvedTransforms.value(i, KoSvgText::CharTransformation());
1330
1331 // Function to get the delta position.
1332 auto getDelta = [res, totalStartDelta, accumulatedOffset, anchorAbsolute, anchorCssPos, startAdvance] (QPointF pos) -> QPointF {
1333 QPointF delta = pos - (res.textPathAndAnchoringOffset + anchorAbsolute + res.textLengthOffset);
1334 delta -= (totalStartDelta + accumulatedOffset + (res.cssPosition-anchorCssPos) + startAdvance);
1335 return delta;
1336 };
1337 // Function to get absolute position.
1338 auto getAbsolute = [res, tf, anchorAbsolute] (QPointF pos) -> QPointF {
1339 QPointF p = pos - (res.textPathAndAnchoringOffset - anchorAbsolute) - tf.relativeOffset();
1340 return p;
1341 };
1342
1343 if (i < startIndex) {
1344 if (!deltaPosition) {
1345 // Because we don't split the text content element, we need to set the absolute pos for every preceding transform.
1346 const QPointF p = getAbsolute(res.finalPosition);
1347 if (!tf.xPos) {
1348 tf.xPos = p.x();
1349 }
1350 if (!tf.yPos) {
1351 tf.yPos = p.y();
1352 }
1353 }
1354 transforms << tf;
1355 continue;
1356 }
1357
1358 if (i >= endIndex) {
1359 if (i == endIndex) {
1360 // Counter transform to keep unselected characters at the same pos.
1361 if (deltaPosition && !tf.startsNewChunk()) {
1362 QPointF delta = getDelta(res.finalPosition);
1363 tf.dxPos = delta.x();
1364 tf.dyPos = delta.y();
1365 } else {
1366 const QPointF p = getAbsolute(res.finalPosition) - anchorAbsolute;
1367 tf.xPos = p.x();
1368 tf.yPos = p.y();
1369 }
1370 if (!tf.rotate) {
1371 tf.rotate = 0.0;
1372 }
1373 }
1374 transforms << tf;
1375 continue;
1376 }
1377
1378 if (rotateDegrees.size()+startIndex > i) {
1379 tf.rotate = kisDegreesToRadians(rotateDegrees.value(transformIndex, tf.rotate? *tf.rotate: rotateDegrees.last()));
1380 }
1381 if (positions.size()+startIndex > i) {
1382 const QPointF pos = positions.value(transformIndex, QPointF());
1383
1384 if (deltaPosition) {
1385 if (tf.startsNewChunk()) {
1386 anchorAbsolute = tf.absolutePos();
1387 totalStartDelta = tf.relativeOffset();
1388 anchorCssPos = res.cssPosition;
1389 }
1390
1391 QPointF delta = getDelta(pos);
1392 tf.dxPos = delta.x();
1393 tf.dyPos = delta.y();
1394
1395 if (tf.startsNewChunk()) {
1396 accumulatedOffset = QPointF();
1397 totalStartDelta = tf.relativeOffset();
1398 } else {
1399 accumulatedOffset += tf.relativeOffset();
1400 }
1401 } else {
1402 const QPointF delta = getAbsolute(pos);
1403 tf.xPos = delta.x();
1404 tf.yPos = delta.y();
1405 accumulatedOffset = pos - res.finalPosition;
1406 }
1407
1408 }
1409
1410 transforms << tf;
1411 }
1412 it->localTransformations = transforms;
1413 changed = true;
1414 }
1415 currentIndex = it->finalResultIndex;
1416 if (currentIndex > endIndex) {
1417 break;
1418 }
1419 }
1420 const CharacterResult res = d->result.last();
1421
1422 if (changed) {
1423 KoSvgTextShape::Private::cleanUp(d->textData);
1424 notifyChanged();
1426 }
1427
1428 return changed;
1429}
1430
1433 info.finalPos = res.finalPosition;
1435 info.visualIndex = res.visualIndex;
1436 info.middle = res.middle;
1437 info.advance = res.advance;
1438 info.logicalIndex = index;
1439 info.rtl = res.cursorInfo.rtl;
1440 info.metrics = res.metrics;
1441 return info;
1442}
1443
1445{
1447 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1448 return infos;
1449 }
1450 const int finalPos = d->cursorPos.size()-1;
1451 const int startIndex = d->getCursorPos(qMin(startPos, endPos)).index;
1452 const int endIndex = d->getCursorPos(qMax(startPos, endPos)).index;
1453
1454 for (int i = startIndex; i < endIndex; i++) {
1455 CharacterResult res = d->result.value(i);
1456 if (!res.addressable) continue;
1457 infos << infoFromCharacterResult(res, i);
1458 }
1459
1460 if (endIndex == startIndex) {
1461 bool final = qMax(startPos, endPos) == finalPos;
1462 CharacterResult resFinal = final? d->result.last(): d->result.value(startIndex);
1463 if (resFinal.addressable) {
1464 infos << infoFromCharacterResult(resFinal, startIndex);
1465 }
1466 }
1467
1468 return infos;
1469}
1470
1471void KoSvgTextShape::removeTransformsFromRange(const int startPos, const int endPos)
1472{
1473 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1474 return;
1475 }
1476 const int startIndex = d->getCursorPos(qMin(startPos, endPos)).index;
1477 const int endIndex = d->getCursorPos(qMax(startPos, endPos)).index;
1478
1479 d->removeTransforms(d->textData, startIndex, endIndex-startIndex);
1480
1481 KoSvgTextShape::Private::cleanUp(d->textData);
1482 notifyChanged();
1484}
1485
1487 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1488 if (child->properties.hasProperty(propertyId)) {
1489 return child;
1490 } else if (KisForestDetail::childBegin(child) != KisForestDetail::childEnd(child)) {
1491 auto found = findNodeIndexForPropertyIdImpl(child, propertyId);
1492 if (found != child) {
1493 return found;
1494 }
1495 }
1496 }
1497 return parent;
1498}
1499
1501{
1502 for (auto it = d->textData.childBegin(); it != d->textData.childEnd(); it++) {
1503 if (it->properties.hasProperty(propertyId)) {
1504 return d->createTextNodeIndex(it);
1506 auto found = findNodeIndexForPropertyIdImpl(it, propertyId);
1507 if (found != it) {
1508 return d->createTextNodeIndex(found);
1509 }
1510 }
1511 }
1512 return d->createTextNodeIndex(d->textData.childBegin());
1513}
1514
1516{
1517 int startIndex = 0;
1518 int endIndex = 0;
1519 for (auto child = d->textData.childBegin(); child != d->textData.childEnd(); child++) {
1520 // count children
1521 d->startIndexOfIterator(child, node.d->textElement, startIndex);
1522 endIndex = d->numChars(node.d->textElement) + startIndex;
1523 }
1524 return qMakePair(posForIndex(startIndex), posForIndex(endIndex));
1525}
1526
1528{
1529 auto candidate = d->textData.childBegin();
1530 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(candidate);
1531 if (childBegin(d->textData.childBegin()) != childEnd(d->textData.childBegin())) {
1532 candidate = childBegin(d->textData.childBegin());
1533 }
1534 const int index = d->getCursorPos(pos).index;
1535 int currentIndex = 0;
1536
1537 auto e = Private::findTextContentElementForIndex(d->textData, currentIndex, index, true);
1538 if (e == d->textData.depthFirstTailEnd()) {
1539 return d->createTextNodeIndex(candidate);
1540 }
1541
1542 auto element = KisForestDetail::siblingCurrent(e);
1543 auto parent = Private::findTopLevelParent(d->textData.childBegin(), element);
1544 if (parent == childEnd(d->textData.childBegin())) return d->createTextNodeIndex(candidate);
1545
1546 return d->createTextNodeIndex(parent);
1547}
1548
1550{
1551 auto root = d->textData.childBegin();
1552 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(root);
1553
1554 for (auto child = childBegin(root); child != childEnd(root); child++) {
1555 if (child->textPathId == textPath->name()) {
1556 return d->createTextNodeIndex(child);
1557 }
1558 }
1559 return d->createTextNodeIndex(root);
1560}
1561
1563{
1564 return KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1565}
1566
1575
1577{
1578 if (KisForestDetail::size(d->textData) == 0) {
1579 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1580 }
1581 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::FillId,
1582 QVariant::fromValue(KoSvgText::BackgroundProperty(background)));
1583
1585 notifyChanged();
1586}
1587
1589{
1590 KoSvgTextProperties props = KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1592}
1593
1595{
1596 if (KisForestDetail::size(d->textData) == 0) {
1597 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1598 }
1599 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::StrokeId,
1600 QVariant::fromValue(KoSvgText::StrokeProperty(stroke)));
1602 notifyChanged();
1603}
1604
1613
1615{
1616 if (KisForestDetail::size(d->textData) == 0) {
1617 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1618 }
1619 KIS_SAFE_ASSERT_RECOVER_RETURN(first != second);
1621
1622 if (first != Fill) {
1623 if (order.at(1) == first) {
1624 order[1] = order[0];
1625 order[0] = first;
1626 } else if (order.at(2) == first) {
1627 order[2] = order[0];
1628 order[0] = first;
1629 }
1630 }
1631 if (second != first && second != Stroke) {
1632 if (order.at(2) == second) {
1633 order[2] = order[1];
1634 order[1] = second;
1635 }
1636 }
1637 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::PaintOrder,
1638 QVariant::fromValue(order));
1639 setInheritPaintOrder(false);
1640}
1641
1643{
1644 return d->plainText;
1645}
1646
1651
1656
1658{
1659 if (d->textData.empty()) return false;
1660 return (KisForestDetail::size(d->textData) == 1);
1661}
1662
1664{
1665 if (d->isLoading) {
1666 return;
1667 }
1668 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1669 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1670 if (cursorListener) {
1671 cursorListener->notifyCursorPosChanged(pos, anchor);
1672 }
1673 }
1674}
1675
1677{
1678 if (d->isLoading) {
1679 return;
1680 }
1681 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1682 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1683 if (cursorListener) {
1684 cursorListener->notifyMarkupChanged();
1685 }
1686 }
1687}
1688
1690{
1691 const int inlineSize = writingMode() == KoSvgText::HorizontalTB? outlineRect().width(): outlineRect().height();
1692 d->applyWhiteSpace(d->textData, true);
1693 d->insertNewLinesAtAnchors(d->textData, !d->shapesInside.isEmpty());
1694 d->cleanUp(d->textData);
1695
1696 KoSvgTextProperties props = this->propertiesForPos(-1);
1697 if (makeInlineSize) {
1699 // Using QCeil here because otherwise the text will layout too tight.
1700 val.customValue = qCeil(inlineSize);
1701 val.isAuto = false;
1703 props.setProperty(KoSvgTextProperties::InlineSizeId, QVariant::fromValue(val));
1704 }
1705 } else {
1707 }
1708 // NOTE: applyWhiteSpace and insertNewLines don't notify changes,
1709 // so setProperties is the only thing triggering relayout();
1710 setPropertiesAtPos(-1, props);
1711}
1712
1714{
1715 if (d->result.isEmpty()) return;
1716 d->setTransformsFromLayout(d->textData, d->result);
1717 d->cleanUp(d->textData);
1718 //d->applyWhiteSpace(d->textData, true);
1719 KoSvgTextProperties props = this->propertiesForPos(-1);
1723
1724 setPropertiesAtPos(-1, props);
1725}
1726
1727#include "KoXmlWriter.h"
1728#include "SvgWriter.h"
1730{
1731 bool success = false;
1732
1733 QList<KoShape*> visibleShapes;
1734 Q_FOREACH(KoShape *shape, d->internalShapes()) {
1735 if (shape->isVisible(false)) {
1736 visibleShapes.append(shape);
1737 }
1738 }
1739 const bool writeGroup = !(visibleShapes.isEmpty() || context.strippedTextMode());
1740 if (writeGroup) {
1741 context.shapeWriter().startElement("g", false);
1742 context.shapeWriter().addAttribute("id", context.createUID("group"));
1743 context.shapeWriter().addAttribute(KoSvgTextShape_TEXTCONTOURGROUP, "true");
1744
1746 SvgWriter writer(visibleShapes);
1747 writer.saveDetached(context);
1748 }
1749 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1750 if (it.state() == KisForestDetail::Enter) {
1751 bool isTextPath = false;
1752 QMap<QString, QString> shapeSpecificStyles;
1753 if (!it->textPathId.isEmpty()) {
1754 isTextPath = true;
1755 }
1756 if (it == d->textData.compositionBegin()) {
1757 context.shapeWriter().startElement("text", false);
1758
1759 if (!context.strippedTextMode()) {
1760 context.shapeWriter().addAttribute("id", context.getID(this));
1761
1762 // save the version to distinguish from the buggy Krita version
1763 // 2: Wrong font-size.
1764 // 3: Wrong font-size-adjust.
1765 context.shapeWriter().addAttribute("krita:textVersion", 3);
1766
1767 if (visibleShapes.isEmpty()) {
1769 }
1770 SvgStyleWriter::saveSvgStyle(this, context);
1771 } else {
1772 SvgStyleWriter::saveSvgFill(this->background(), false, this->outlineRect(), this->size(), this->absoluteTransformation(), context);
1773 SvgStyleWriter::saveSvgStroke(this->stroke(), context);
1775 inheritPaintOrder(), context, true);
1776 }
1777 shapeSpecificStyles = this->shapeTypeSpecificStyles(context);
1778 } else {
1779 if (isTextPath) {
1780 context.shapeWriter().startElement("textPath", false);
1781 } else {
1782 context.shapeWriter().startElement("tspan", false);
1783 }
1784 SvgStyleWriter::saveSvgBasicStyle(it->properties.property(KoSvgTextProperties::Visibility, true).toBool(),
1785 it->properties.property(KoSvgTextProperties::Opacity, 0).toReal(),
1786 it->properties.property(KoSvgTextProperties::PaintOrder,
1787 QVariant::fromValue(paintOrder())
1789 !it->properties.hasProperty(KoSvgTextProperties::PaintOrder), context, true);
1790
1791 }
1792
1793 KoShape *textPath = KoSvgTextShape::Private::textPathByName(it->textPathId, d->textPaths);
1794 success = it->saveSvg(context,
1795 it == d->textData.compositionBegin(),
1796 d->childCount(siblingCurrent(it)) == 0,
1797 shapeSpecificStyles,
1798 textPath);
1799 } else {
1800 if (it == d->textData.compositionBegin()) {
1801 SvgStyleWriter::saveMetadata(this, context);
1802 }
1803 context.shapeWriter().endElement();
1804 }
1805 }
1806 if (writeGroup) {
1807 context.shapeWriter().endElement();
1808 }
1809 return success;
1810}
1811
1813{
1814 bool success = true;
1816 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1817 if (it.state() == KisForestDetail::Enter) {
1818 QMap<QString, QString> shapeSpecificStyles;
1819
1820 if (it == d->textData.compositionBegin()) {
1821 context.shapeWriter().startElement("p", false);
1822 } else {
1823 context.shapeWriter().startElement("span", false);
1824 }
1825 KoSvgTextProperties ownProperties = it->properties.ownProperties(parentProps.last(),
1826 it == d->textData.compositionBegin());
1827 parentProps.append(ownProperties);
1828 QMap<QString, QString> attributes = ownProperties.convertToSvgTextAttributes();
1829 if (it == d->textData.compositionBegin())
1830 attributes.insert(ownProperties.convertParagraphProperties());
1831 bool addedFill = false;
1832 if (attributes.size() > 0) {
1833 QString styleString;
1834 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
1835 if (QString(it.key().toLatin1().data()).contains("text-anchor")) {
1836 QString val = it.value();
1837 if (it.value()=="middle") {
1838 val = "center";
1839 } else if (it.value()=="end") {
1840 val = "right";
1841 } else {
1842 val = "left";
1843 }
1844 styleString.append("text-align")
1845 .append(": ")
1846 .append(val)
1847 .append(";" );
1848 } else if (QString(it.key().toLatin1().data()).contains("fill")) {
1849 styleString.append("color")
1850 .append(": ")
1851 .append(it.value())
1852 .append(";" );
1853 addedFill = true;
1854 } else if (QString(it.key().toLatin1().data()).contains("font-size")) {
1855 QString val = it.value();
1856 styleString.append(it.key().toLatin1().data())
1857 .append(": ")
1858 .append(val)
1859 .append(";" );
1860 } else {
1861 styleString.append(it.key().toLatin1().data())
1862 .append(": ")
1863 .append(it.value())
1864 .append(";" );
1865 }
1866 }
1867 if (ownProperties.hasProperty(KoSvgTextProperties::FillId) && !addedFill) {
1868 KoColorBackground *b = dynamic_cast<KoColorBackground *>(it->properties.background().data());
1869 if (b) {
1870 styleString.append("color")
1871 .append(": ")
1872 .append(b->color().name())
1873 .append(";" );
1874 }
1875 }
1876 context.shapeWriter().addAttribute("style", styleString);
1877
1878 if (d->childCount(siblingCurrent(it)) == 0) {
1879 debugFlake << "saveHTML" << this << it->text;
1880 // After adding all the styling to the <p> element, add the text
1881 context.shapeWriter().addTextNode(it->text);
1882 }
1883 }
1884 } else {
1885 parentProps.pop_back();
1886 context.shapeWriter().endElement();
1887 }
1888 }
1889 return success;
1890}
1891
1893{
1894
1895}
1896
1898{
1899
1900}
1901
1903{
1905 d->result,
1906 d->lineBoxes,
1907 d->cursorPos,
1908 d->logicalToVisualCursorPos,
1909 d->plainText,
1910 d->isBidi,
1911 d->initialTextPosition));
1912}
1913
1915{
1916 // TODO: add an assert that all linked shpaes in memento are present in
1917 // the current state of d->textPaths. That is the responsibility of
1918 // KoSvgTextAddRemoveShapeCommandImpl to prepare the shapes for us
1919
1920 KoSvgTextShapeMementoImpl *impl = dynamic_cast<KoSvgTextShapeMementoImpl*>(memento.data());
1921 if (impl) {
1922 d->textData = impl->textData;
1923 d->result = impl->result;
1924 d->lineBoxes = impl->lineBoxes;
1925 d->cursorPos = impl->cursorPos;
1926 d->logicalToVisualCursorPos = impl->logicalToVisualCursorPos;
1927 d->plainText = impl->plainText;
1928 d->isBidi = impl->isBidi;
1929 d->initialTextPosition = impl->initialTextPosition;
1930
1931 // Ensure that any text paths exist.
1932 auto root = d->textData.childBegin();
1933 for (auto child = childBegin(root); child != childEnd(root); child++) {
1934 if (!child->textPathId.isEmpty()) {
1935 KIS_SAFE_ASSERT_RECOVER(Private::textPathByName(child->textPathId, d->textPaths)) {
1936 qDebug() << "missing path is" << child->textPathId;
1937 child->textPathId = QString();
1938 }
1939 }
1940 }
1941 }
1942}
1943
1945{
1946 setMementoImpl(memento);
1947 if (d->bulkActionState) {
1948 d->bulkActionState->layoutSetFromMemento = true;
1949 }
1950 notifyChanged();
1953}
1954
1955void KoSvgTextShape::setMemento(const KoSvgTextShapeMementoSP memento, int pos, int anchor)
1956{
1957 const bool shapeOffsetBefore = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1959 setMementoImpl(memento);
1960 const bool shapeOffsetAfter = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1962 if (shapeOffsetBefore || shapeOffsetAfter) {
1963 if (d->bulkActionState) {
1964 d->bulkActionState->contourHasChanged = true;
1965 } else {
1966 d->updateTextWrappingAreas();
1967 }
1968 } else {
1969 if (d->bulkActionState) {
1970 d->bulkActionState->layoutSetFromMemento = true;
1971 }
1972 }
1973 notifyChanged();
1976}
1977
1979{
1980 qDebug() << "Tree size:" << KisForestDetail::size(d->textData);
1981 QString spaces;
1982 for (auto it = compositionBegin(d->textData); it != compositionEnd(d->textData); it++) {
1983 if (it.state() == KisForestDetail::Enter) {
1984
1985 qDebug() << QString(spaces + "+") << it->text;
1986 qDebug() << QString(spaces + "|") << it->properties.convertToSvgTextAttributes();
1987 qDebug() << QString(spaces + "| PropertyType:") << it->properties.property(KoSvgTextProperties::KraTextStyleType).toString();
1988 qDebug() << QString(spaces + "| Fill set: ") << it->properties.hasProperty(KoSvgTextProperties::FillId);
1989 qDebug() << QString(spaces + "| Stroke set: ") << it->properties.hasProperty(KoSvgTextProperties::StrokeId);
1990 qDebug() << QString(spaces + "| Opacity: ") << it->properties.property(KoSvgTextProperties::Opacity);
1991 qDebug() << QString(spaces + "| PaintOrder: ") << it->properties.hasProperty(KoSvgTextProperties::PaintOrder);
1992 qDebug() << QString(spaces + "| Visibility set: ") << it->properties.hasProperty(KoSvgTextProperties::Visibility);
1993 qDebug() << QString(spaces + "| TextPath set: ") << it->textPathId;
1994 qDebug() << QString(spaces + "| Transforms set: ") << it->localTransformations;
1995 spaces.append(" ");
1996 }
1997
1998 if (it.state() == KisForestDetail::Leave) {
1999 spaces.chop(1);
2000 }
2001 }
2002}
2003
2005{
2006 d->isLoading = disable;
2007}
2008
2010{
2011 return d->isLoading;
2012}
2013
2015{
2016 d->disableFontMatching = disable;
2017}
2018
2020{
2021 return d->disableFontMatching;
2022}
2023
2025{
2026 return Private::generateShapes(shapesInside, shapesSubtract, props);
2027}
2028
2030{
2031 KIS_SAFE_ASSERT_RECOVER_RETURN(!d->bulkActionState);
2032 d->bulkActionState.emplace(boundingRect());
2033}
2034
2036{
2038
2039 QRectF updateRect;
2040
2041 if (d->bulkActionState->changed()) {
2042 if (d->bulkActionState->contourHasChanged) {
2043 d->updateTextWrappingAreas();
2044 } else if (d->bulkActionState->layoutHasChanged) {
2045 // updateTextWrappingAreas() already includes a call to relayout()
2046 relayout();
2047 }
2048 // otherwise, it's an update from a memento.
2049
2050 updateRect = d->bulkActionState->originalBoundingRect | boundingRect();
2051 }
2052
2053 d->bulkActionState = std::nullopt;
2054 return updateRect;
2055}
2056
2057void KoSvgTextShape::paint(QPainter &painter) const
2058{
2059 painter.save();
2060
2061 painter.setTransform(d->shapeGroup->absoluteTransformation().inverted()*painter.transform());
2062 d->internalShapesPainter->paint(painter);
2063 painter.restore();
2064
2065 painter.save();
2067 if (textRendering == KoSvgText::RenderingOptimizeSpeed || !painter.testRenderHint(QPainter::Antialiasing)) {
2068 // also apply antialiasing only if antialiasing is active on provided target QPainter
2069 painter.setRenderHint(QPainter::Antialiasing, false);
2070 painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
2071 } else {
2072 painter.setRenderHint(QPainter::Antialiasing, true);
2073 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
2074 }
2075
2076 QPainterPath chunk;
2077 int currentIndex = 0;
2078 if (!d->result.isEmpty()) {
2079 QPainterPath rootBounds;
2080 rootBounds.addRect(Private::boundingBoxFromTree(d->textData, this, false));
2081 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationUnderline, textRendering);
2082 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationOverline, textRendering);
2083 d->paintPaths(painter, rootBounds, this, d->result, textRendering, chunk, currentIndex);
2084 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationLineThrough, textRendering);
2085 }
2086
2087 painter.restore();
2088}
2089
2090void KoSvgTextShape::paintStroke(QPainter &painter) const
2091{
2092 Q_UNUSED(painter);
2093 // do nothing! everything is painted in paint()
2094}
2095
2096QPainterPath KoSvgTextShape::outline() const {
2097 QPainterPath result;
2098 if (!d->internalShapes().isEmpty()) {
2099 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2100 result.addPath(shape->transformation().map(shape->outline()));
2101 }
2102 }
2103
2104 if ((d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2105 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2106 result.addPath(it->associatedOutline);
2107 for (int i = 0; i < it->textDecorations.values().size(); ++i) {
2108 result.addPath(it->textDecorations.values().at(i));
2109 }
2110 }
2111 }
2112
2113 return result;
2114}
2116{
2117 return outline().boundingRect();
2118}
2119
2121{
2122 QRectF shapesRect;
2123 if (d->internalShapesPainter->contentRect().isValid()) {
2124 shapesRect = d->internalShapesPainter->contentRect();
2125 if (!(d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2126 return shapesRect;
2127 }
2128 }
2129 QRectF result = Private::boundingBoxFromTree(d->textData, this, true);
2130
2131 return (this->absoluteTransformation().mapRect(result) | shapesRect);
2132}
2133
2135{
2136 // TODO: check if KoShape::m_d->size is consistent, check cache in KoShapeGroup::size()
2137 return outlineRect().size();
2138}
2139
2140void KoSvgTextShape::setSize(const QSizeF &size)
2141{
2142 const QRectF oRect = this->outlineRect();
2143 const QSizeF oldSize = oRect.size();
2144
2145 if (size == oldSize) return;
2146
2147 // don't try to divide by zero
2148 if (oldSize.isEmpty()) return;
2149
2150 const qreal scaleX = size.width() / oldSize.width();
2151 const qreal scaleY = size.height() / oldSize.height();
2152
2153 if (d->internalShapes().isEmpty()) {
2159 const qreal scaleX = size.width() / oldSize.width();
2160 const qreal scaleY = size.height() / oldSize.height();
2161
2162 this->scale(scaleX, scaleY);
2163 // TODO: use scaling function for kosvgtextproperties when styles presets are merged.
2164 notifyChanged();
2166 } else {
2167 if (!d->textPaths.isEmpty()) return;
2168
2169 const bool allInternalShapeAreTranslatedOnly = [this] () {
2170 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2171 if (shape->transformation().type() > QTransform::TxTranslate) {
2172 return false;
2173 }
2174 }
2175 return true;
2176 }();
2177
2178 if (allInternalShapeAreTranslatedOnly) {
2183 Q_FOREACH (KoShape *internalShape, d->internalShapes()) {
2190 const QPointF stillPoint = this->absoluteTransformation().map(QPointF());
2191 const bool useGlobalMode = false;
2192 const bool usePostScaling = false;
2193 KoFlake::resizeShapeCommon(internalShape,
2194 scaleX,
2195 scaleY,
2196 stillPoint,
2197 useGlobalMode,
2198 usePostScaling,
2199 QTransform());
2200 }
2201
2202 const QSizeF realNewSize = outlineRect().size();
2203 KoShape::setSize(realNewSize);
2204 } else {
2248 const QTransform scale = QTransform::fromScale(scaleX, scaleY);
2249
2250 Q_FOREACH (KoShape *shape, d->internalShapes()) {
2251 shape->setTransformation(shape->transformation() * scale);
2252 }
2253
2254 const QSizeF realNewSize = outlineRect().size();
2255 KoShape::setSize(realNewSize);
2256 }
2257 }
2258}
2259
2260void KoSvgTextShape::paintDebug(QPainter &painter, const DebugElements elements) const
2261{
2262 if (elements & DebugElement::CharBbox) {
2263 int currentIndex = 0;
2264 if (!d->result.isEmpty()) {
2265 QPainterPath rootBounds;
2266 rootBounds.addRect(this->outline().boundingRect());
2267 d->paintDebug(painter, d->result, currentIndex);
2268 }
2269
2270 //Debug shape outlines.
2271 Q_FOREACH (KoShape *shapeInside, d->shapesInside) {
2272 QPainterPath p = shapeInside->outline();
2273 p = shapeInside->transformation().map(p);
2274 painter.strokePath(p, QPen(Qt::green));
2275 }
2276 Q_FOREACH (KoShape *shapeInside, d->shapesSubtract) {
2277 QPainterPath p = shapeInside->outline();
2278 p = shapeInside->transformation().map(p);
2279 painter.strokePath(p, QPen(Qt::red));
2280 }
2281 }
2282
2283 if (elements & DebugElement::LineBox) {
2284 Q_FOREACH (LineBox lineBox, d->lineBoxes) {
2285 Q_FOREACH (const LineChunk &chunk, lineBox.chunks) {
2286 QPen pen;
2287 pen.setCosmetic(true);
2288 pen.setWidth(2);
2289 painter.setBrush(QBrush(Qt::transparent));
2290 pen.setColor(QColor(0, 128, 255, 128));
2291 painter.setPen(pen);
2292 painter.drawLine(chunk.length);
2293 pen.setColor(QColor(255, 128, 0, 128));
2294 painter.setPen(pen);
2295 painter.drawRect(chunk.boundingBox);
2296
2297 pen.setColor(QColor(255, 0, 0, 128));
2298 pen.setStyle(Qt::DashDotDotLine);
2299 painter.setPen(pen);
2300 painter.drawLine(chunk.length.translated(lineBox.baselineTop));
2301 pen.setColor(QColor(0, 128, 0, 128));
2302 pen.setStyle(Qt::DashDotLine);
2303 painter.setPen(pen);
2304 painter.drawLine(chunk.length.translated(lineBox.baselineBottom));
2305 }
2306 }
2307 }
2308}
2309
2311{
2312 KoShape *shape = nullptr;
2313 int currentIndex = 0;
2314 if (!d->result.empty()) {
2315 shape = d->collectPaths(this, d->result, currentIndex);
2316 }
2317
2318 return shape;
2319}
2320
2322{
2324 if (!d->shapesInside.isEmpty()) {
2325 return TextType::TextInShape;
2326 } else if (!inlineSize.isAuto) {
2327 return TextType::InlineWrap;
2328 } else {
2329 bool textSpaceCollapse = false;
2330 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2331 KoSvgText::TextSpaceCollapse collapse = KoSvgText::TextSpaceCollapse(it->properties.propertyOrDefault(KoSvgTextProperties::TextCollapseId).toInt());
2332 if (collapse == KoSvgText::Collapse || collapse == KoSvgText::PreserveSpaces) {
2333 textSpaceCollapse = true;
2334 break;
2335 }
2336 if (!it->localTransformations.isEmpty()) {
2337 textSpaceCollapse = true;
2338 break;
2339 }
2340 }
2342 }
2344}
2345
2347{
2348 removeShapesFromContours(d->shapesInside, false);
2350}
2351
2353{
2354 Q_FOREACH(KoShape *shape, shapes) {
2355 if (d->textPaths.contains(shape)) {
2356 d->removeTextPathId(d->textData.childBegin(), shape->name());
2357 d->cleanUp(d->textData);
2358 d->textPaths.removeAll(shape);
2359 }
2360
2361 if (inside) {
2362 if (d->shapesSubtract.contains(shape)) {
2363 d->shapesSubtract.removeAll(shape);
2364 }
2365 d->shapesInside.append(shape);
2366 } else {
2367 if (d->shapesInside.contains(shape)) {
2368 d->shapesInside.removeAll(shape);
2369 }
2370 d->shapesSubtract.append(shape);
2371 }
2372 if (!d->shapeGroup->shapes().contains(shape)) {
2373 d->shapeGroup->addShape(shape);
2374 shape->addDependee(this);
2375 }
2376 }
2377
2378 notifyChanged(); // notify shape manager that our geometry has changed
2379
2380 if (!d->isLoading) {
2381 d->updateTextWrappingAreas();
2382 }
2383
2384 d->updateInternalShapesList();
2386 update();
2387}
2388
2390{
2391 return (shape->parent() == d->shapeGroup.data());
2392}
2393
2394void KoSvgTextShape::removeShapesFromContours(QList<KoShape *> shapes, bool callUpdate, bool cleanup)
2395{
2396 Q_FOREACH(KoShape *shape, shapes) {
2397 if (shape) {
2398 d->removeTextPathId(d->textData.childBegin(), shape->name());
2399 shape->removeDependee(this);
2400 d->shapesInside.removeAll(shape);
2401 d->shapesSubtract.removeAll(shape);
2402 d->textPaths.removeAll(shape);
2403
2404 }
2405 d->shapeGroup->removeShape(shape);
2406 }
2407 if (cleanup) {
2408 d->cleanUp(d->textData);
2409 }
2410 if (callUpdate) {
2411 notifyChanged(); // notify shape manager that our geometry has changed
2412 if (!d->isLoading) {
2413 d->updateTextWrappingAreas();
2414 }
2415 d->updateInternalShapesList();
2417 update();
2418 }
2419}
2420
2421void KoSvgTextShape::moveShapeInsideToIndex(KoShape *shapeInside, const int index)
2422{
2423 const int oldIndex = d->shapesInside.indexOf(shapeInside);
2424 if (oldIndex < 0) return;
2425
2426 // Update.
2427 d->shapesInside.move(oldIndex, index);
2428 if (!d->isLoading) {
2429 d->updateTextWrappingAreas();
2430 }
2431 d->updateInternalShapesList();
2433 update();
2434}
2435
2436bool KoSvgTextShape::setTextPathOnRange(KoShape *textPath, const int startPos, const int endPos)
2437{
2438 const int finalPos = d->cursorPos.size() - 1;
2439 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->getCursorPos(startPos).index;
2440 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->getCursorPos(endPos).index;
2441
2442 Private::splitTree(d->textData, startIndex, false);
2443 Private::splitTree(d->textData, endIndex, true);
2444 int currentIndex = 0;
2445
2446 Private::makeTextPathNameUnique(d->textPaths, textPath);
2447 KoSvgTextContentElement textPathElement;
2448 textPathElement.textPathId = textPath->name();
2449 d->shapeGroup->addShape(textPath);
2450 d->textPaths.append(textPath);
2451 textPath->addDependee(this);
2452
2453
2454 if (KisForestDetail::depth(d->textData) == 1) {
2455 textPathElement.text = d->textData.childBegin()->text;
2459 Q_FOREACH(KoSvgTextProperties::PropertyId p, copyIds) {
2460 if (d->textData.childBegin()->properties.hasProperty(KoSvgTextProperties::TextDecorationLineId)) {
2461 textPathElement.properties.setProperty(p, d->textData.childBegin()->properties.property(p));
2462 d->textData.childBegin()->properties.removeProperty(p);
2463 }
2464 }
2465
2466 d->textData.childBegin()->text = QString();
2467 d->textData.insert(childEnd(d->textData.childBegin()), textPathElement);
2468 } else {
2469 // find nodes
2470 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2471 currentIndex = 0;
2472 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2473
2474 auto first = startElement.node()?Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2475 : childEnd(d->textData.childBegin());
2476 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2477 : childEnd(d->textData.childBegin());
2478
2479 // move children. we collect them before inserting the text path,
2480 // so we don't get iterator issues.
2482 auto textPathIt = textPathTree.insert(
2483 textPathTree.childEnd(),
2484 textPathElement);
2486 for (auto child = first;
2487 (child != last && child != childEnd(d->textData.childBegin()));
2488 child++) {
2489 if (!child->textPathId.isEmpty()) {
2490 Q_FOREACH(KoShape *shape, d->textPaths) {
2491 if (shape->name() == child->textPathId) {
2492 removeShapesFromContours({shape}, false, false);
2493 break;
2494 }
2495 }
2496 child->textPathId = QString();
2497 }
2498 movableChildren.append(child);
2499 }
2500 while (!movableChildren.isEmpty()) {
2501 auto child = movableChildren.takeLast();
2502 textPathTree.move(child, KisForestDetail::childBegin(textPathIt));
2503 }
2504 d->textData.move(textPathIt, last);
2505 }
2506 Private::cleanUp(d->textData);
2507
2508 d->updateInternalShapesList();
2509 notifyChanged();
2511 update();
2512 return true;
2513}
2514
2515QList<KoShape *> KoSvgTextShape::textPathsAtRange(const int startPos, const int endPos)
2516{
2517 const int finalPos = d->cursorPos.size() - 1;
2518 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->getCursorPos(startPos).index;
2519 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->getCursorPos(endPos).index;
2521 int currentIndex = 0;
2522 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2523 currentIndex = 0;
2524 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2525
2526 auto first = startElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2527 : childEnd(d->textData.childBegin());
2528 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2529 : childEnd(d->textData.childBegin());
2530 if (last != childEnd(d->textData.childBegin())) {
2531 last++;
2532 }
2533 for (auto child = first; (child != last && child != KisForestDetail::siblingEnd(first)); child++) {
2534 if (KoShape *path = Private::textPathByName(child->textPathId, d->textPaths)) {
2535 textPaths.append(path);
2536 }
2537 }
2538 return textPaths;
2539}
2540
2542{
2543 auto root = d->textData.childBegin();
2544 if (root == d->textData.childEnd()) return;
2545 if (d->textPaths.contains(textPath)) return;
2546
2547 Private::makeTextPathNameUnique(d->textPaths, textPath);
2548 KoSvgTextContentElement textPathElement;
2549 textPathElement.textPathId = textPath->name();
2550 d->shapeGroup->addShape(textPath);
2551 d->textPaths.append(textPath);
2552 textPath->addDependee(this);
2553
2554 d->textData.insert(childEnd(root), textPathElement);
2555}
2556
2558{
2559 return d->shapesInside;
2560}
2561
2563{
2564 removeShapesFromContours(d->shapesSubtract, false);
2566}
2567
2569{
2570 return d->currentTextWrappingAreas;
2571}
2572
2574{
2575 return d->shapesSubtract;
2576}
2577
2579{
2580 return d->internalShapesPainter->internalShapeManager();
2581}
2582
2583QMap<QString, QString> KoSvgTextShape::shapeTypeSpecificStyles(SvgSavingContext &context) const
2584{
2585 QMap<QString, QString> map = this->textProperties().convertParagraphProperties();
2586 if (!d->shapesInside.isEmpty()) {
2587 QStringList shapesInsideList;
2588 Q_FOREACH(KoShape* shape, d->shapesInside) {
2589 QString id = (shape->isVisible(false) && !context.strippedTextMode())? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2590 shapesInsideList.append(QString("url(#%1)").arg(id));
2591 }
2592 map.insert("shape-inside", shapesInsideList.join(" "));
2595 map.insert("overflow", "clip");
2596 }
2597 if (!d->shapesSubtract.isEmpty()) {
2598 QStringList shapesInsideList;
2599 Q_FOREACH(KoShape* shape, d->shapesSubtract) {
2600 QString id = shape->isVisible(false)? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2601 shapesInsideList.append(QString("url(#%1)").arg(id));
2602 }
2603 map.insert("shape-subtract", shapesInsideList.join(" "));
2604 }
2605
2606 return map;
2607}
2608
2610{
2611 d->relayout();
2612}
2613
2615 : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18nc("Text label in SVG Text Tool", "Text"))
2616{
2617 setToolTip(i18n("SVG Text Shape"));
2618 setIconName(koIconNameCStr("x-shape-text"));
2621
2623 t.name = i18n("SVG Text");
2624 t.iconName = koIconName("x-shape-text");
2625 t.toolTip = i18n("SVG Text Shape");
2626 addTemplate(t);
2627}
2628
2630{
2631 Q_UNUSED(documentResources);
2632 debugFlake << "Create default svg text shape";
2633
2634 KoSvgTextShape *shape = new KoSvgTextShape();
2636 shape->insertText(0, i18nc("Default text for the text shape", "Placeholder Text"));
2637
2638 return shape;
2639}
2640
2642{
2643 KoSvgTextShape *shape = new KoSvgTextShape();
2645
2646 QString svgText = params->stringProperty("svgText", i18nc("Default text for the text shape", "<text>Placeholder Text</text>"));
2647 QString defs = params->stringProperty("defs" , "<defs/>");
2648 QRectF shapeRect = QRectF(0, 0, 200, 60);
2649 QVariant rect = params->property("shapeRect");
2650 QVariant origin = params->property("origin");
2651
2652#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2653 if (rect.type()==QVariant::RectF) {
2654#else
2655 if (rect.typeId() == QMetaType::QRectF) {
2656#endif
2657 shapeRect = rect.toRectF();
2658 }
2659
2660 KoSvgTextShapeMarkupConverter converter(shape);
2661 converter.convertFromSvg(svgText,
2662 defs,
2663 shapeRect,
2664 documentResources->documentResolution());
2665#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2666 if (origin.type() == QVariant::PointF) {
2667#else
2668 if (origin.typeId() == QMetaType::QPointF) {
2669#endif
2670 shape->setPosition(origin.toPointF());
2671 } else {
2672 shape->setPosition(shapeRect.topLeft());
2673 }
2675
2676 return shape;
2677}
2678
2679bool KoSvgTextShapeFactory::supports(const QDomElement &/*e*/, KoShapeLoadingContext &/*context*/) const
2680{
2681 return false;
2682}
2683
2685{
2686 Q_UNUSED(type);
2687 Q_UNUSED(shape);
2688}
2689
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:554
KoShapeAnchor * anchor() const
virtual QVector< PaintOrder > paintOrder() const
paintOrder
Definition KoShape.cpp:688
static QVector< PaintOrder > defaultPaintOrder()
default paint order as per SVG specification
Definition KoShape.cpp:699
virtual void update() const
Definition KoShape.cpp:529
virtual void shapeChanged(ChangeType type, KoShape *shape=0)
Definition KoShape.cpp:1054
KoShapeContainer * parent() const
Definition KoShape.cpp:857
void shapeChangedPriv(KoShape::ChangeType type)
Definition KoShape.cpp:105
QTransform absoluteTransformation() const
Definition KoShape.cpp:330
void setTransformation(const QTransform &matrix)
Definition KoShape.cpp:369
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:210
void removeDependee(KoShape *shape)
Definition KoShape.cpp:1038
QTransform transformation() const
Returns the shapes local transformation matrix.
Definition KoShape.cpp:378
bool inheritPaintOrder() const
inheritPaintOrder
Definition KoShape.cpp:710
virtual void setPosition(const QPointF &position)
Set the position of the shape in pt.
Definition KoShape.cpp:268
virtual void updateAbsolute(const QRectF &rect) const
Definition KoShape.cpp:540
@ Stroke
Definition KoShape.h:117
void notifyChanged()
Definition KoShape.cpp:613
bool addDependee(KoShape *shape)
Definition KoShape.cpp:1021
void setShapeId(const QString &id)
Definition KoShape.cpp:880
void setInheritPaintOrder(bool value)
setInheritPaintOrder set inherit paint order.
Definition KoShape.cpp:705
QString name() const
Definition KoShape.cpp:950
bool isVisible(bool recursive=true) const
Definition KoShape.cpp:797
virtual void setSize(const QSizeF &size)
Resize the shape.
Definition KoShape.cpp:249
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)
QVariant propertyOrDefault(PropertyId id) const
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)
KoSvgText::Direction direction() const
direction Whether the text is left to right or right to left.
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.
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.