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->cursorPos.at(pos);
329 if (d->result.at(p.cluster).anchored_chunk && p.offset == 0) {
330 return pos;
331 }
332 int candidate = 0;
333 for (int i = 0; i < pos; i++) {
334 CursorPos p2 = d->cursorPos.at(i);
335 if (d->result.at(p2.cluster).anchored_chunk && p2.offset == 0) {
336 candidate = i;
337 }
338 }
339 return candidate;
340}
341
343{
344 if (pos < 0 || d->cursorPos.isEmpty() || d->result.isEmpty()) {
345 return pos;
346 }
347 if (pos > d->cursorPos.size() - 1) {
348 return d->cursorPos.size() - 1;
349 }
350 int candidate = 0;
351 int posCluster = d->cursorPos.at(pos).cluster;
352 for (int i = pos; i < d->cursorPos.size(); i++) {
353 CursorPos p = d->cursorPos.at(i);
354 if (d->result.at(p.cluster).anchored_chunk && i > pos && posCluster != p.cluster) {
355 break;
356 }
357 candidate = i;
358 }
359 return candidate;
360}
361
362int KoSvgTextShape::wordLeft(int pos, bool visual)
363{
364 //TODO: figure out preferred behaviour for wordLeft in RTL && visual.
365 Q_UNUSED(visual)
366 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
367 return pos;
368 }
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->cursorPos.at(qBound(0, pos, d->cursorPos.size())).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->cursorPos.at(qBound(0, pos, d->cursorPos.size())).index;
410
411 for (int i = pos; i >= 0; i--) {
412 if (d->cursorPos.at(i).index < currentIndex) {
413 return i;
414 }
415 }
416 return pos;
417}
418
419int KoSvgTextShape::nextPos(int pos, bool visual)
420{
421 if (d->cursorPos.isEmpty()) {
422 return -1;
423 }
424
425 if(visual) {
426 int visualIndex = d->logicalToVisualCursorPos.value(pos);
427 return d->logicalToVisualCursorPos.key(qMin(visualIndex + 1, d->cursorPos.size() - 1), d->cursorPos.size() - 1);
428 }
429
430 return qMin(pos + 1, d->cursorPos.size() - 1);
431}
432
433int KoSvgTextShape::previousPos(int pos, bool visual)
434{
435 if (d->cursorPos.isEmpty()) {
436 return -1;
437 }
438
439 if(visual) {
440 int visualIndex = d->logicalToVisualCursorPos.value(pos);
441 return d->logicalToVisualCursorPos.key(qMax(visualIndex - 1, 0), 0);
442 }
443
444 return qMax(pos - 1, 0);
445}
446
448{
449 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
450 return pos;
451 }
452
453 int nextLineStart = lineEnd(pos)+1;
454 int nextLineEnd = lineEnd(nextLineStart);
455 CursorPos cursorPos = d->cursorPos.at(pos);
456 if (!this->shapesInside().isEmpty()) {
457 LineBox nextLineBox;
458 for (int i = 0; i < d->lineBoxes.size(); ++i) {
459 for (int j = 0; j < d->lineBoxes.at(i).chunks.size(); ++j) {
460 if (d->lineBoxes.at(i).chunks.at(j).chunkIndices.contains(cursorPos.cluster)) {
461 nextLineBox = d->lineBoxes.at(qMin(i + 1, d->lineBoxes.size()-1));
462 }
463 }
464 }
465 if (nextLineBox.chunks.size()>0) {
466 int first = -1;
467 int last = -1;
468 Q_FOREACH(LineChunk chunk, nextLineBox.chunks) {
469 Q_FOREACH (int i, chunk.chunkIndices) {
470 if (d->result.at(i).addressable) {
471 if (first < 0) {
472 first = d->result.at(i).cursorInfo.graphemeIndices.first();
473 }
474 last = d->result.at(i).cursorInfo.graphemeIndices.last();
475 }
476 }
477 }
478 if (first > -1 && last > -1) {
479 nextLineStart = posForIndex(first);
480 nextLineEnd = posForIndex(last);
481 }
482 }
483 }
484
485
486 if (nextLineStart > d->cursorPos.size()-1) {
487 return d->cursorPos.size()-1;
488 }
489
490 CharacterResult res = d->result.at(cursorPos.cluster);
491 QPointF currentPoint = res.finalPosition;
492 currentPoint += res.cursorInfo.offsets.value(cursorPos.offset, res.advance);
493 int candidate = posForPoint(currentPoint, nextLineStart, nextLineEnd+1);
494 if (candidate < 0) {
495 return pos;
496 }
497
498 return candidate;
499}
500
502{
503 if (pos < 0 || pos > d->cursorPos.size()-1 || d->result.isEmpty() || d->cursorPos.isEmpty()) {
504 return pos;
505 }
506 int currentLineStart = lineStart(pos);
507 if (currentLineStart - 1 < 0) {
508 return 0;
509 }
510 int previousLineStart = lineStart(currentLineStart-1);
511
512 CursorPos cursorPos = d->cursorPos.at(pos);
513 if (!this->shapesInside().isEmpty()) {
514 LineBox previousLineBox;
515 for (int i = 0; i < d->lineBoxes.size(); ++i) {
516 for (int j = 0; j < d->lineBoxes.at(i).chunks.size(); ++j) {
517 if (d->lineBoxes.at(i).chunks.at(j).chunkIndices.contains(cursorPos.cluster)) {
518 previousLineBox = d->lineBoxes.at(qMax(i - 1, 0));
519 }
520 }
521 }
522 if (previousLineBox.chunks.size()>0) {
523 int first = -1;
524 int last = -1;
525 Q_FOREACH(LineChunk chunk, previousLineBox.chunks) {
526 Q_FOREACH (int i, chunk.chunkIndices) {
527 if (d->result.at(i).addressable) {
528 if (first < 0) {
529 first = d->result.at(i).cursorInfo.graphemeIndices.first();
530 }
531 last = d->result.at(i).cursorInfo.graphemeIndices.last();
532 }
533 }
534 }
535 if (first > -1 && last > -1) {
536 previousLineStart = posForIndex(first);
537 currentLineStart = posForIndex(last);
538 }
539 }
540 }
541
542 CharacterResult res = d->result.at(cursorPos.cluster);
543 QPointF currentPoint = res.finalPosition;
544 currentPoint += res.cursorInfo.offsets.value(cursorPos.offset, res.advance);
545 int candidate = posForPoint(currentPoint, previousLineStart, currentLineStart);
546 if (candidate < 0) {
547 return pos;
548 }
549
550 return candidate;
551}
552
554{
555 if (pos < 0 || pos >= d->cursorPos.size() || d->result.isEmpty() || d->cursorPos.isEmpty()) {
556 return pos;
557 }
558
559 int wordEnd = pos;
560 for (int i = pos; i < d->cursorPos.size(); i++) {
561 wordEnd = i;
562 CursorPos cursorPos = d->cursorPos.at(i);
563 bool isWhiteSpace = false;
564 const CharacterResult res = d->result.at(cursorPos.cluster);
565 const int pIndex = res.plaintTextIndex;
566 if (pIndex >= 0 && pIndex < d->plainText.size()) {
567 QChar c = d->plainText.at(pIndex);
568 isWhiteSpace = KoCssTextUtils::IsCssWordSeparator(QString(c)) || c.isSpace();
569 }
571 && (cursorPos.offset == 0 || cursorPos.offset+1 == res.cursorInfo.offsets.size())
572 && i > pos) {
573
574 if (isWhiteSpace) {
575 wordEnd = qMax(pos, wordEnd-1);
576 }
577 break;
578 }
579
580 }
581
582 return wordEnd;
583}
584
586{
587 if (pos <= 0 || pos > d->cursorPos.size() || d->result.isEmpty() || d->cursorPos.isEmpty()) {
588 return pos;
589 }
590
591 int wordStart = pos;
592 for (int i = pos; i >= 0; i--) {
593 wordStart = i;
594 CursorPos cursorPos = d->cursorPos.at(i);
595 const CharacterResult res = d->result.at(cursorPos.cluster);
597 && (cursorPos.offset == 0 || cursorPos.offset+1 == res.cursorInfo.offsets.size())
598 && i < pos) {
599 break;
600 }
601 }
602
603 return wordStart;
604}
605
607{
609 double fontSize = this->textProperties().fontSize().value;
610 QPainterPath p;
611 if (mode == KoSvgText::HorizontalTB) {
612 p.moveTo(0, fontSize*0.2);
613 p.lineTo(0, -fontSize);
614 } else {
615 p.moveTo(-fontSize * 0.5, 0);
616 p.lineTo(fontSize, 0);
617 }
618 p.translate(d->initialTextPosition);
619
620 return p;
621}
622
623QPainterPath KoSvgTextShape::cursorForPos(int pos, QLineF &caret, QColor &color, double bidiFlagSize)
624{
625 if (d->result.isEmpty() || d->cursorPos.isEmpty() || pos < 0 || pos >= d->cursorPos.size()) {
626 return defaultCursorShape();
627 }
628 QPainterPath p;
629
630 CursorPos cursorPos = d->cursorPos.at(pos);
631
632 CharacterResult res = d->result.at(cursorPos.cluster);
633
634 const QTransform tf = res.finalTransform();
635 color = res.cursorInfo.color;
636 caret = res.cursorInfo.caret;
637 caret.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
638
639 p.moveTo(tf.map(caret.p1()));
640 p.lineTo(tf.map(caret.p2()));
641 if (d->isBidi && bidiFlagSize > 0) {
642 int sign = res.cursorInfo.rtl ? -1 : 1;
643 double bidiFlagHalf = bidiFlagSize * 0.5;
644 QPointF point3;
645 QPointF point4;
647 qreal slope = bidiFlagHalf * (caret.dx()/ caret.dy());
648 point3 = QPointF(caret.p2().x() + slope + (sign * bidiFlagHalf), caret.p2().y() + bidiFlagHalf);
649 point4 = QPointF(point3.x() + slope - (sign * bidiFlagHalf),point3.y() + bidiFlagHalf);
650 } else {
651 qreal slope = bidiFlagHalf * (caret.dy()/ caret.dx());
652 point3 = QPointF(caret.p2().x() - bidiFlagHalf, caret.p2().y() - slope + (sign * bidiFlagHalf));
653 point4 = QPointF(point3.x() - bidiFlagHalf, point3.y() - slope - (sign * bidiFlagHalf));
654 }
655 p.lineTo(tf.map(point3));
656 p.lineTo(tf.map(point4));
657 }
658 caret = tf.map(caret);
659
660 return p;
661}
662
663QPainterPath KoSvgTextShape::selectionBoxes(int pos, int anchor)
664{
665 int start = qMin(pos, anchor);
666 int end = qMax(pos, anchor);
667 end = qMin(d->cursorPos.size()-1, end);
668
669 if (start == end || start < 0) {
670 return QPainterPath();
671 }
672
673 QPainterPath p;
674 p.setFillRule(Qt::WindingFill);
675 for (int i = start+1; i <= end; i++) {
676 CursorPos cursorPos = d->cursorPos.at(i);
677 CharacterResult res = d->result.at(cursorPos.cluster);
678 const QTransform tf = res.finalTransform();
679 QLineF first = res.cursorInfo.caret;
680 QLineF last = first;
681 if (res.cursorInfo.rtl) {
682 last.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, res.advance));
683 first.translate(res.cursorInfo.offsets.value(cursorPos.offset, QPointF()));
684 } else {
685 first.translate(res.cursorInfo.offsets.value(cursorPos.offset-1, QPointF()));
686 last.translate(res.cursorInfo.offsets.value(cursorPos.offset, res.advance));
687 }
688 QPolygonF poly;
689 poly << first.p1() << first.p2() << last.p2() << last.p1() << first.p1();
690 p.addPolygon(tf.map(poly));
691 }
692
693 return p;
694}
695
696QPainterPath KoSvgTextShape::underlines(int pos, int anchor, KoSvgText::TextDecorations decor, KoSvgText::TextDecorationStyle style, qreal minimum, bool thick)
697{
698 int start = qMin(pos, anchor);
699 int end = qMax(pos, anchor);
700
701 if (start == end || start < 0 || end >= d->cursorPos.size()) {
702 return QPainterPath();
703 }
704 QPainterPathStroker stroker;
705
707 stroker.setCapStyle(Qt::FlatCap);
708 if (style == KoSvgText::Solid) {
709 stroker.setDashPattern(Qt::SolidLine);
710 } else if (style == KoSvgText::Dashed) {
711 stroker.setDashPattern(Qt::DashLine);
712 } else if (style == KoSvgText::Dotted) {
713 stroker.setDashPattern(Qt::DotLine);
714 } else {
715 stroker.setDashPattern(Qt::SolidLine);
716 }
717
718 QPainterPath underPath;
719 QPainterPath overPath;
720 QPainterPath middlePath;
721 qint32 strokeWidth = 0;
722 QPointF inset = mode == KoSvgText::HorizontalTB? QPointF(minimum*0.5, 0): QPointF(0, minimum*0.5);
723 for (int i = start+1; i <= end; i++) {
724 CursorPos pos = d->cursorPos.at(i);
725 CharacterResult res = d->result.at(pos.cluster);
726 strokeWidth += res.metrics.underlineThickness;
727 const QTransform tf = res.finalTransform();
728 QPointF first = res.cursorInfo.caret.p1();
729 QPointF last = first;
730 if (res.cursorInfo.rtl) {
731 last += res.cursorInfo.offsets.value(pos.offset-1, res.advance);
732 first += res.cursorInfo.offsets.value(pos.offset, QPointF());
733 if (i == start+1) {
734 first -= inset;
735 }
736 if (i == end) {
737 last += inset;
738 }
739 } else {
740 first += res.cursorInfo.offsets.value(pos.offset-1, QPointF());
741 last += res.cursorInfo.offsets.value(pos.offset, res.advance);
742 if (i == start+1) {
743 first += inset;
744 }
745 if (i == end) {
746 last -= inset;
747 }
748 }
749
750 if (decor.testFlag(KoSvgText::DecorationUnderline)){
751 underPath.moveTo(tf.map(first));
752 underPath.lineTo(tf.map(last));
753 }
754 QPointF diff = res.cursorInfo.caret.p2() - res.cursorInfo.caret.p1();
755 if (decor.testFlag(KoSvgText::DecorationOverline)){
756 overPath.moveTo(tf.map(first+diff));
757 overPath.lineTo(tf.map(last+diff));
758 }
759 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
760 middlePath.moveTo(tf.map(first+(diff*0.5)));
761 middlePath.lineTo(tf.map(last+(diff*0.5)));
762 }
763 }
764
765 const qreal freetypePixelsToPt = (1.0 / 64.0) * (72. / qMin(d->xRes, d->yRes));
766 const qreal width = strokeWidth > 0 ? qMax(qreal(strokeWidth/qMax(1, end-(start+1)))*freetypePixelsToPt, minimum): minimum;
767
768 stroker.setWidth(thick? width*2: width);
769
770 QPainterPath final;
771 if (decor.testFlag(KoSvgText::DecorationUnderline)){
772 final.addPath(stroker.createStroke(underPath));
773 }
774 if (decor.testFlag(KoSvgText::DecorationOverline)){
775 final.addPath(stroker.createStroke(overPath));
776 }
777 if (decor.testFlag(KoSvgText::DecorationLineThrough)){
778 final.addPath(stroker.createStroke(middlePath));
779 }
780
781 return final;
782}
783
784int KoSvgTextShape::posForPoint(QPointF point, int start, int end, bool *overlaps)
785{
786 int a = 0;
787 int b = d->cursorPos.size();
788 if (start >= 0 && end >= 0) {
789 a = qMax(start, a);
790 b = qMin(end, b);
791 }
792 double closest = std::numeric_limits<double>::max();
793 int candidate = 0;
794 for (int i = a; i < b; i++) {
795 CursorPos pos = d->cursorPos.at(i);
796 CharacterResult res = d->result.at(pos.cluster);
797 QPointF cursorStart = res.finalPosition;
798 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
799 double distance = kisDistance(cursorStart, point);
800 if (distance < closest) {
801 candidate = i;
802 closest = distance;
803 if (overlaps) {
804 *overlaps = res.finalTransform().map(res.layoutBox()).containsPoint(point, Qt::WindingFill);
805 }
806 }
807 }
808 return candidate;
809}
810
812{
813 bool overlaps = false;
814 int initialPos = posForPoint(point, -1, -1, &overlaps);
815
816 if (overlaps) {
817 return initialPos;
818 }
819
821
822 int candidateLineStart = 0;
823 double closest = std::numeric_limits<double>::max();
824 for (int i = 0; i < d->cursorPos.size(); i++) {
825 CursorPos pos = d->cursorPos.at(i);
826 CharacterResult res = d->result.at(pos.cluster);
827 if (res.anchored_chunk) {
828 QLineF caret = res.cursorInfo.caret;
829 caret.translate(res.finalPosition);
830 QPointF cursorStart = res.finalPosition;
831 cursorStart += res.cursorInfo.offsets.value(pos.offset, res.advance);
832 double distance = kisDistance(cursorStart, point);
833 if (mode == KoSvgText::HorizontalTB) {
834 if (caret.p1().y() > point.y() && caret.p2().y() <= point.y() && closest > distance) {
835 candidateLineStart = i;
836 closest = distance;
837 }
838 } else {
839 if (caret.p2().x() > point.x() && caret.p1().x() <= point.x() && closest > distance) {
840 candidateLineStart = i;
841 closest = distance;
842 }
843 }
844 }
845 }
846
847 if (candidateLineStart > -1) {
848 int end = lineEnd(candidateLineStart);
849 initialPos = posForPoint(point, candidateLineStart, qMin(end + 1, d->cursorPos.size()));
850 }
851
852 return initialPos;
853}
854
855int KoSvgTextShape::posForIndex(int index, bool firstIndex, bool skipSynthetic) const
856{
857 int pos = -1;
858 if (d->cursorPos.isEmpty() || index < 0) {
859 return pos;
860 }
861 for (int i = 0; i< d->cursorPos.size(); i++) {
862 if (skipSynthetic && d->cursorPos.at(i).synthetic) {
863 continue;
864 }
865 if (d->cursorPos.at(i).index <= index) {
866 pos = i;
867 if (d->cursorPos.at(i).index == index && firstIndex) {
868 break;
869 }
870 } else if (d->cursorPos.at(i).index > index) {
871 break;
872 }
873 }
874
875 return pos;
876}
877
879{
880 if (d->cursorPos.isEmpty() || pos < 0) {
881 return -1;
882 }
883
884 return d->cursorPos.at(qMin(d->cursorPos.size()-1, pos)).index;
885}
886
888{
889 return d->initialTextPosition;
890}
891
892bool KoSvgTextShape::insertText(int pos, QString text)
893{
894 bool success = false;
895 int currentIndex = 0;
896
900 int elementIndex = 0;
901 int insertionIndex = 0;
902 if (pos > -1 && !d->cursorPos.isEmpty()) {
903 CursorPos cursorPos = d->cursorPos.at(pos);
904 CharacterResult res = d->result.at(cursorPos.cluster);
905 elementIndex = res.plaintTextIndex;
906 insertionIndex = cursorPos.index;
907 elementIndex = qMin(elementIndex, d->result.size()-1);
908 }
909 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
910 if (it != d->textData.depthFirstTailEnd()) {
911 const int offset = insertionIndex - currentIndex;
912 it->insertText(offset, text);
913
914 d->insertTransforms(d->textData, insertionIndex, text.size(), (elementIndex == insertionIndex));
917 success = true;
918 }
919 return success;
920}
921
922bool KoSvgTextShape::removeText(int &index, int &length)
923{
924 bool success = false;
925 if (index < -1) {
926 return success;
927 }
928 int currentLength = length;
929 int endLength = 0;
930 while (currentLength > 0) {
931 int currentIndex = 0;
932
933 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, index, true);
934 if (it != d->textData.depthFirstTailEnd()) {
935 int offset = index > currentIndex? index - currentIndex: 0;
936 int size = it->numChars(false);
937 it->removeText(offset, currentLength);
938 int diff = size - it->numChars(false);
939 currentLength -= diff;
940 endLength += diff;
941
942 if (index >= currentIndex) {
943 index = currentIndex + offset;
944 }
945
946 d->removeTransforms(d->textData, index, endLength);
947
948 success = true;
949 } else {
950 currentLength = -1;
951 }
952 }
953 if (success) {
954 length = endLength;
957 }
958 return success;
959}
960
961KoSvgTextProperties KoSvgTextShape::propertiesForPos(const int pos, bool inherited) const
962{
963 return propertiesForRange(pos, pos, inherited).value(0, KoSvgTextProperties());
964}
965
968 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it));
969 parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) {
970 hierarchy.append(parentIt);
971 }
973 while (!hierarchy.isEmpty()) {
974 auto it = hierarchy.takeLast();
975 KoSvgTextProperties p = it->properties;
976 p.inheritFrom(props, true);
977 props = p;
978 }
979 return props;
980}
981
982QList<KoSvgTextProperties> KoSvgTextShape::propertiesForRange(const int startPos, const int endPos, bool inherited) const
983{
985
987 if (d->isLoading) return props;
988
989 if (((startPos < 0 || startPos >= d->cursorPos.size()) && startPos == endPos) || d->cursorPos.isEmpty()) {
990 props = {KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties()};
991 return props;
992 }
993 const int finalPos = d->cursorPos.size() - 1;
994 const int startIndex = d->cursorPos.at(qBound(0, startPos, finalPos)).index;
995 const int endIndex = d->cursorPos.at(qBound(0, endPos, finalPos)).index;
996 int sought = startIndex;
997 if (startIndex == endIndex) {
998 int currentIndex = 0;
999 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1000 if (it != d->textData.depthFirstTailEnd()) {
1001 if (inherited) {
1002 props.append(inheritProperties(it));
1003 } else {
1004 props.append(it->properties);
1005 }
1006 } else {
1007 currentIndex = 0;
1008 it = d->findTextContentElementForIndex(d->textData, currentIndex, sought - 1);
1009 if (it != d->textData.depthFirstTailEnd()) {
1010 if (inherited) {
1011 props.append(inheritProperties(it));
1012 } else {
1013 props.append(it->properties);
1014 }
1015 }
1016 }
1017 } else {
1018 while(sought < endIndex) {
1019 int currentIndex = 0;
1020 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, sought);
1021 if (KisForestDetail::siblingCurrent(it) == d->textData.childBegin()) {
1022 // If there's a selection and the search algorithm only returns the root, return empty.
1023 // The root text properties should be retrieved explicitly (either by using -1 as pos, or by calling textProperties()).
1024 props = {KoSvgTextProperties()};
1025 return props;
1026 } else if (it != d->textData.depthFirstTailEnd()) {
1027 if (inherited) {
1028 props.append(inheritProperties(it));
1029 } else {
1030 props.append(it->properties);
1031 }
1032 }
1033 sought = currentIndex + it->numChars(false);
1034 }
1035 }
1036
1037 return props;
1038}
1039
1041{
1042 if (pos < 0 || d->cursorPos.isEmpty()) {
1043 if (KisForestDetail::size(d->textData)) {
1044 d->textData.childBegin()->properties = properties;
1045 }
1046 notifyChanged();
1048 return;
1049 }
1050 CursorPos cursorPos = d->cursorPos.at(pos);
1051 CharacterResult res = d->result.at(cursorPos.cluster);
1052 int currentIndex = 0;
1053 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, res.plaintTextIndex);
1054 if (it != d->textData.depthFirstTailEnd()) {
1055 it->properties = properties;
1056 notifyChanged();
1058 }
1059}
1060
1062 const int endPos,
1063 const KoSvgTextProperties properties,
1064 const QSet<KoSvgTextProperties::PropertyId> removeProperties)
1065{
1066 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1067 if (KisForestDetail::size(d->textData)) {
1068 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1069 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1070 }
1071 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1072 d->textData.childBegin()->properties.removeProperty(p);
1073 }
1074 }
1075 notifyChanged();
1078 || removeProperties.contains(KoSvgTextProperties::ShapePaddingId)
1079 || removeProperties.contains(KoSvgTextProperties::ShapeMarginId)) {
1081 } else {
1083 }
1084 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1086 }
1089 }
1090 return;
1091 }
1092 const int startIndex = d->cursorPos.at(startPos).index;
1093 const int endIndex = d->cursorPos.at(endPos).index;
1094 if (startIndex != endIndex) {
1095 KoSvgTextShape::Private::splitContentElement(d->textData, startIndex);
1096 KoSvgTextShape::Private::splitContentElement(d->textData, endIndex);
1097 }
1098 bool changed = false;
1099 int currentIndex = 0;
1100 KoSvgText::AutoValue inlineSize = d->textData.childBegin()->properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
1101 bool isWrapping = !d->shapesInside.isEmpty() || !inlineSize.isAuto;
1102 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1103 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0) {
1104 continue;
1105 }
1106
1107 if (currentIndex >= startIndex && currentIndex < endIndex) {
1108 Q_FOREACH(KoSvgTextProperties::PropertyId p, removeProperties) {
1110 d->textData.childBegin()->properties.removeProperty(p);
1111 } else {
1112 it->properties.removeProperty(p);
1113 }
1114 }
1115 Q_FOREACH(KoSvgTextProperties::PropertyId p, properties.properties()) {
1117 d->textData.childBegin()->properties.setProperty(p, properties.property(p));
1118 } else {
1119 it->properties.setProperty(p, properties.property(p));
1120 }
1121 }
1122
1123 changed = true;
1124 }
1125 currentIndex += it->numChars(false);
1126 }
1127
1128 if (changed){
1129 KoSvgTextShape::Private::cleanUp(d->textData);
1130 notifyChanged();
1132 if (properties.hasProperty(KoSvgTextProperties::FillId)) {
1134 }
1137 }
1138 }
1139}
1140
1141std::unique_ptr<KoSvgTextShape> KoSvgTextShape::copyRange(int index, int length) const
1142{
1143 KoSvgTextShape *clone = new KoSvgTextShape(*this);
1144 int zero = 0;
1145 int endRange = index + length;
1146 int size = KoSvgTextShape::Private::numChars(clone->d->textData.childBegin(), false) - endRange;
1147 clone->removeText(endRange, size);
1148 clone->removeText(zero, index);
1149 KoSvgTextShape::Private::cleanUp(clone->d->textData);
1150 return std::unique_ptr<KoSvgTextShape>(clone);
1151}
1152
1153bool KoSvgTextShape::insertRichText(int pos, const KoSvgTextShape *richText, bool inheritPropertiesIfPossible)
1154{
1155 bool success = false;
1156 int currentIndex = 0;
1157 int elementIndex = 0;
1158 int insertionIndex = 0;
1159
1160 if (isEnd(richText->d->textData.childBegin())) {
1161 // rich text is empty.
1162 return success;
1163 }
1164
1165 if (pos > -1 && !d->cursorPos.isEmpty()) {
1166 CursorPos cursorPos = d->cursorPos.at(qMin(d->cursorPos.size()-1, pos));
1167 CharacterResult res = d->result.at(cursorPos.cluster);
1168 elementIndex = res.plaintTextIndex;
1169 insertionIndex = cursorPos.index;
1170 elementIndex = qMin(elementIndex, d->result.size()-1);
1171 }
1172
1173 KoSvgTextShape::Private::splitContentElement(this->d->textData, insertionIndex);
1174
1175 auto it = d->findTextContentElementForIndex(d->textData, currentIndex, insertionIndex);
1176 auto richTextIt = d->textData.childEnd();
1177 if (it != d->textData.depthFirstTailEnd()) {
1178 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingCurrent(it));
1179 success = true;
1180 } else {
1181 currentIndex = 0;
1182 it = d->findTextContentElementForIndex(d->textData, currentIndex, elementIndex);
1183 if (it != d->textData.depthFirstTailEnd()) {
1184 richTextIt = d->textData.move(richText->d->textData.childBegin(), siblingEnd(siblingCurrent(it)));
1185 success = true;
1186 }
1187 }
1188
1189 if (richTextIt != d->textData.childEnd()) {
1190 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1192 richTextIt->properties.removeProperty(p);
1193 }
1194 }
1195 auto parentIt = KisForestDetail::hierarchyBegin(richTextIt);
1196 auto parentEnd = KisForestDetail::hierarchyEnd(richTextIt);
1197 if (parentIt != parentEnd) {
1198 parentIt++;
1199 if (inheritPropertiesIfPossible && parentIt != parentEnd) {
1200 Q_FOREACH (const KoSvgTextProperties::PropertyId p, richTextIt->properties.properties()) {
1201 if (richTextIt->properties.inheritsProperty(p, parentIt->properties)) {
1202 richTextIt->properties.removeProperty(p);
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209 if (success) {
1210 notifyChanged();
1212 }
1213 return success;
1214}
1215
1217{
1218 KoSvgTextShape::Private::cleanUp(d->textData);
1219 notifyChanged();
1221}
1222
1223bool KoSvgTextShape::setCharacterTransformsOnRange(const int startPos, const int endPos, const QVector<QPointF> positions, const QVector<qreal> rotateDegrees, const bool deltaPosition)
1224{
1225 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1226 return false;
1227 }
1228 const int finalPos = d->cursorPos.size()-1;
1229 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1230 int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1231 if (startIndex == endIndex) {
1232 endIndex += 1;
1233 while(d->result.at(endIndex).middle) {
1234 endIndex += 1;
1235 if (endIndex > d->result.size()) break;
1236 }
1237 }
1238
1239 bool changed = false;
1240
1241 QVector<KoSvgText::CharTransformation> resolvedTransforms = Private::resolvedTransformsForTree(d->textData, !shapesInside().isEmpty(), true);
1242 Private::removeTransforms(d->textData, startIndex, endIndex-startIndex);
1243 QPointF totalStartDelta;
1244 QPointF anchorAbsolute;
1245 QPointF anchorCssPos;
1246
1247 const KoSvgText::Direction dir = KoSvgText::Direction(this->textProperties().propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
1249
1250 const CharacterResult startRes = d->result.value(startIndex);
1251 const QPointF startAdvance = startRes.cursorInfo.rtl? startRes.advance: QPointF();
1252
1253 if (deltaPosition) {
1254 for (int i = 0; i< startIndex; i++) {
1255 KoSvgText::CharTransformation tf = resolvedTransforms.value(i);
1256 if (tf.xPos) {
1257 totalStartDelta.setX(0);
1258 anchorAbsolute.setX(*tf.xPos);
1259 }
1260 if (tf.yPos) {
1261 totalStartDelta.setY(0);
1262 anchorAbsolute.setY(*tf.yPos);
1263 }
1264 if (tf.startsNewChunk()) {
1265 const CharacterResult res = d->result.value(i);
1266 anchorCssPos = res.cssPosition;
1267 }
1268 totalStartDelta += tf.relativeOffset();
1269 }
1270 } else {
1271 bool rtl = (dir == KoSvgText::DirectionRightToLeft);
1272 QPointF positionAtVisualEnd = (rtl? d->result.first(): d->result.last()).finalPosition;
1274 anchorAbsolute = positionAtVisualEnd;
1275 } else if (anchor == KoSvgText::AnchorMiddle) {
1276 anchorAbsolute = positionAtVisualEnd/2;
1277 }
1278 }
1279
1280 int currentIndex = 0;
1281 QPointF accumulatedOffset;
1282 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
1283 if (KoSvgTextShape::Private::childCount(siblingCurrent(it)) > 0 || !it->textPathId.isEmpty()) {
1284 continue;
1285 }
1286
1287 int endContentElement = it->finalResultIndex;
1288
1289 if (endContentElement >= startIndex && currentIndex <= endIndex) {
1290
1292 int addressableOffset = 0;
1293 for (int i = currentIndex; (i < endContentElement); i++) {
1294 const CharacterResult res = d->result.value(i);
1295 if (!res.addressable) {
1296 addressableOffset += 1;
1297 continue;
1298 }
1299
1300 const int transformIndex = (i - startIndex) - addressableOffset;
1301 KoSvgText::CharTransformation tf = resolvedTransforms.value(i, KoSvgText::CharTransformation());
1302
1303 // Function to get the delta position.
1304 auto getDelta = [res, totalStartDelta, accumulatedOffset, anchorAbsolute, anchorCssPos, startAdvance] (QPointF pos) -> QPointF {
1305 QPointF delta = pos - (res.textPathAndAnchoringOffset + anchorAbsolute + res.textLengthOffset);
1306 delta -= (totalStartDelta + accumulatedOffset + (res.cssPosition-anchorCssPos) + startAdvance);
1307 return delta;
1308 };
1309 // Function to get absolute position.
1310 auto getAbsolute = [res, tf, anchorAbsolute] (QPointF pos) -> QPointF {
1311 QPointF p = pos - (res.textPathAndAnchoringOffset - anchorAbsolute) - tf.relativeOffset();
1312 return p;
1313 };
1314
1315 if (i < startIndex) {
1316 if (!deltaPosition) {
1317 // Because we don't split the text content element, we need to set the absolute pos for every preceding transform.
1318 const QPointF p = getAbsolute(res.finalPosition);
1319 if (!tf.xPos) {
1320 tf.xPos = p.x();
1321 }
1322 if (!tf.yPos) {
1323 tf.yPos = p.y();
1324 }
1325 }
1326 transforms << tf;
1327 continue;
1328 }
1329
1330 if (i >= endIndex) {
1331 if (i == endIndex) {
1332 // Counter transform to keep unselected characters at the same pos.
1333 if (deltaPosition && !tf.startsNewChunk()) {
1334 QPointF delta = getDelta(res.finalPosition);
1335 tf.dxPos = delta.x();
1336 tf.dyPos = delta.y();
1337 } else {
1338 const QPointF p = getAbsolute(res.finalPosition) - anchorAbsolute;
1339 tf.xPos = p.x();
1340 tf.yPos = p.y();
1341 }
1342 if (!tf.rotate) {
1343 tf.rotate = 0.0;
1344 }
1345 }
1346 transforms << tf;
1347 continue;
1348 }
1349
1350 if (rotateDegrees.size()+startIndex > i) {
1351 tf.rotate = kisDegreesToRadians(rotateDegrees.value(transformIndex, tf.rotate? *tf.rotate: rotateDegrees.last()));
1352 }
1353 if (positions.size()+startIndex > i) {
1354 const QPointF pos = positions.value(transformIndex, QPointF());
1355
1356 if (deltaPosition) {
1357 if (tf.startsNewChunk()) {
1358 anchorAbsolute = tf.absolutePos();
1359 totalStartDelta = tf.relativeOffset();
1360 anchorCssPos = res.cssPosition;
1361 }
1362
1363 QPointF delta = getDelta(pos);
1364 tf.dxPos = delta.x();
1365 tf.dyPos = delta.y();
1366
1367 if (tf.startsNewChunk()) {
1368 accumulatedOffset = QPointF();
1369 totalStartDelta = tf.relativeOffset();
1370 } else {
1371 accumulatedOffset += tf.relativeOffset();
1372 }
1373 } else {
1374 const QPointF delta = getAbsolute(pos);
1375 tf.xPos = delta.x();
1376 tf.yPos = delta.y();
1377 accumulatedOffset = pos - res.finalPosition;
1378 }
1379
1380 }
1381
1382 transforms << tf;
1383 }
1384 it->localTransformations = transforms;
1385 changed = true;
1386 }
1387 currentIndex = it->finalResultIndex;
1388 if (currentIndex > endIndex) {
1389 break;
1390 }
1391 }
1392 const CharacterResult res = d->result.last();
1393
1394 if (changed) {
1395 KoSvgTextShape::Private::cleanUp(d->textData);
1396 notifyChanged();
1398 }
1399
1400 return changed;
1401}
1402
1405 info.finalPos = res.finalPosition;
1407 info.visualIndex = res.visualIndex;
1408 info.middle = res.middle;
1409 info.advance = res.advance;
1410 info.logicalIndex = index;
1411 info.rtl = res.cursorInfo.rtl;
1412 info.metrics = res.metrics;
1413 return info;
1414}
1415
1417{
1419 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1420 return infos;
1421 }
1422 const int finalPos = d->cursorPos.size()-1;
1423 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1424 const int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1425
1426 for (int i = startIndex; i < endIndex; i++) {
1427 CharacterResult res = d->result.value(i);
1428 if (!res.addressable) continue;
1429 infos << infoFromCharacterResult(res, i);
1430 }
1431
1432 if (endIndex == startIndex) {
1433 bool final = qMax(startPos, endPos) == finalPos;
1434 CharacterResult resFinal = final? d->result.last(): d->result.value(startIndex);
1435 if (resFinal.addressable) {
1436 infos << infoFromCharacterResult(resFinal, startIndex);
1437 }
1438 }
1439
1440 return infos;
1441}
1442
1443void KoSvgTextShape::removeTransformsFromRange(const int startPos, const int endPos)
1444{
1445 if ((startPos < 0 && startPos == endPos) || d->cursorPos.isEmpty()) {
1446 return;
1447 }
1448 const int finalPos = d->cursorPos.size()-1;
1449 const int startIndex = d->cursorPos.at(qBound(0, qMin(startPos, endPos), finalPos)).index;
1450 const int endIndex = d->cursorPos.at(qBound(0, qMax(startPos, endPos), finalPos)).index;
1451
1452 d->removeTransforms(d->textData, startIndex, endIndex-startIndex);
1453
1454 KoSvgTextShape::Private::cleanUp(d->textData);
1455 notifyChanged();
1457}
1458
1460 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1461 if (child->properties.hasProperty(propertyId)) {
1462 return child;
1463 } else if (KisForestDetail::childBegin(child) != KisForestDetail::childEnd(child)) {
1464 auto found = findNodeIndexForPropertyIdImpl(child, propertyId);
1465 if (found != child) {
1466 return found;
1467 }
1468 }
1469 }
1470 return parent;
1471}
1472
1474{
1475 for (auto it = d->textData.childBegin(); it != d->textData.childEnd(); it++) {
1476 if (it->properties.hasProperty(propertyId)) {
1477 return d->createTextNodeIndex(it);
1479 auto found = findNodeIndexForPropertyIdImpl(it, propertyId);
1480 if (found != it) {
1481 return d->createTextNodeIndex(found);
1482 }
1483 }
1484 }
1485 return d->createTextNodeIndex(d->textData.childBegin());
1486}
1487
1489{
1490 int startIndex = 0;
1491 int endIndex = 0;
1492 for (auto child = d->textData.childBegin(); child != d->textData.childEnd(); child++) {
1493 // count children
1494 d->startIndexOfIterator(child, node.d->textElement, startIndex);
1495 endIndex = d->numChars(node.d->textElement) + startIndex;
1496 }
1497 return qMakePair(posForIndex(startIndex), posForIndex(endIndex));
1498}
1499
1501{
1502 auto candidate = d->textData.childBegin();
1503 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(candidate);
1504 if (childBegin(d->textData.childBegin()) != childEnd(d->textData.childBegin())) {
1505 candidate = childBegin(d->textData.childBegin());
1506 }
1507 const int finalPos = d->cursorPos.size() - 1;
1508 const int index = d->cursorPos.at(qBound(0, pos, finalPos)).index;
1509 int currentIndex = 0;
1510
1511 auto e = Private::findTextContentElementForIndex(d->textData, currentIndex, index, true);
1512 if (e == d->textData.depthFirstTailEnd()) {
1513 return d->createTextNodeIndex(candidate);
1514 }
1515
1516 auto element = KisForestDetail::siblingCurrent(e);
1517 auto parent = Private::findTopLevelParent(d->textData.childBegin(), element);
1518 if (parent == childEnd(d->textData.childBegin())) return d->createTextNodeIndex(candidate);
1519
1520 return d->createTextNodeIndex(parent);
1521}
1522
1524{
1525 auto root = d->textData.childBegin();
1526 if (d->isLoading || d->cursorPos.isEmpty()) return d->createTextNodeIndex(root);
1527
1528 for (auto child = childBegin(root); child != childEnd(root); child++) {
1529 if (child->textPathId == textPath->name()) {
1530 return d->createTextNodeIndex(child);
1531 }
1532 }
1533 return d->createTextNodeIndex(root);
1534}
1535
1537{
1538 return KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1539}
1540
1549
1551{
1552 if (KisForestDetail::size(d->textData) == 0) {
1553 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1554 }
1555 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::FillId,
1556 QVariant::fromValue(KoSvgText::BackgroundProperty(background)));
1557
1559 notifyChanged();
1560}
1561
1563{
1564 KoSvgTextProperties props = KisForestDetail::size(d->textData)? d->textData.childBegin()->properties: KoSvgTextProperties();
1566}
1567
1569{
1570 if (KisForestDetail::size(d->textData) == 0) {
1571 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1572 }
1573 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::StrokeId,
1574 QVariant::fromValue(KoSvgText::StrokeProperty(stroke)));
1576 notifyChanged();
1577}
1578
1587
1589{
1590 if (KisForestDetail::size(d->textData) == 0) {
1591 d->textData.insert(d->textData.childBegin(), KoSvgTextContentElement());
1592 }
1593 KIS_SAFE_ASSERT_RECOVER_RETURN(first != second);
1595
1596 if (first != Fill) {
1597 if (order.at(1) == first) {
1598 order[1] = order[0];
1599 order[0] = first;
1600 } else if (order.at(2) == first) {
1601 order[2] = order[0];
1602 order[0] = first;
1603 }
1604 }
1605 if (second != first && second != Stroke) {
1606 if (order.at(2) == second) {
1607 order[2] = order[1];
1608 order[1] = second;
1609 }
1610 }
1611 d->textData.childBegin()->properties.setProperty(KoSvgTextProperties::PaintOrder,
1612 QVariant::fromValue(order));
1613 setInheritPaintOrder(false);
1614}
1615
1617{
1618 return d->plainText;
1619}
1620
1625
1630
1632{
1633 if (d->textData.empty()) return false;
1634 return (KisForestDetail::size(d->textData) == 1);
1635}
1636
1638{
1639 if (d->isLoading) {
1640 return;
1641 }
1642 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1643 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1644 if (cursorListener) {
1645 cursorListener->notifyCursorPosChanged(pos, anchor);
1646 }
1647 }
1648}
1649
1651{
1652 if (d->isLoading) {
1653 return;
1654 }
1655 Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) {
1656 TextCursorChangeListener *cursorListener = dynamic_cast<TextCursorChangeListener*>(listener);
1657 if (cursorListener) {
1658 cursorListener->notifyMarkupChanged();
1659 }
1660 }
1661}
1662
1664{
1665 const int inlineSize = writingMode() == KoSvgText::HorizontalTB? outlineRect().width(): outlineRect().height();
1666 d->applyWhiteSpace(d->textData, true);
1667 d->insertNewLinesAtAnchors(d->textData, !d->shapesInside.isEmpty());
1668 d->cleanUp(d->textData);
1669
1670 KoSvgTextProperties props = this->propertiesForPos(-1);
1671 if (makeInlineSize) {
1673 // Using QCeil here because otherwise the text will layout too tight.
1674 val.customValue = qCeil(inlineSize);
1675 val.isAuto = false;
1677 props.setProperty(KoSvgTextProperties::InlineSizeId, QVariant::fromValue(val));
1678 }
1679 } else {
1681 }
1682 // NOTE: applyWhiteSpace and insertNewLines don't notify changes,
1683 // so setProperties is the only thing triggering relayout();
1684 setPropertiesAtPos(-1, props);
1685}
1686
1688{
1689 if (d->result.isEmpty()) return;
1690 d->setTransformsFromLayout(d->textData, d->result);
1691 d->cleanUp(d->textData);
1692 //d->applyWhiteSpace(d->textData, true);
1693 KoSvgTextProperties props = this->propertiesForPos(-1);
1697
1698 setPropertiesAtPos(-1, props);
1699}
1700
1701#include "KoXmlWriter.h"
1702#include "SvgWriter.h"
1704{
1705 bool success = false;
1706
1707 QList<KoShape*> visibleShapes;
1708 Q_FOREACH(KoShape *shape, d->internalShapes()) {
1709 if (shape->isVisible(false)) {
1710 visibleShapes.append(shape);
1711 }
1712 }
1713 const bool writeGroup = !(visibleShapes.isEmpty() || context.strippedTextMode());
1714 if (writeGroup) {
1715 context.shapeWriter().startElement("g", false);
1716 context.shapeWriter().addAttribute("id", context.createUID("group"));
1717 context.shapeWriter().addAttribute(KoSvgTextShape_TEXTCONTOURGROUP, "true");
1718
1720 SvgWriter writer(visibleShapes);
1721 writer.saveDetached(context);
1722 }
1723 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1724 if (it.state() == KisForestDetail::Enter) {
1725 bool isTextPath = false;
1726 QMap<QString, QString> shapeSpecificStyles;
1727 if (!it->textPathId.isEmpty()) {
1728 isTextPath = true;
1729 }
1730 if (it == d->textData.compositionBegin()) {
1731 context.shapeWriter().startElement("text", false);
1732
1733 if (!context.strippedTextMode()) {
1734 context.shapeWriter().addAttribute("id", context.getID(this));
1735
1736 // save the version to distinguish from the buggy Krita version
1737 // 2: Wrong font-size.
1738 // 3: Wrong font-size-adjust.
1739 context.shapeWriter().addAttribute("krita:textVersion", 3);
1740
1741 if (visibleShapes.isEmpty()) {
1743 }
1744 SvgStyleWriter::saveSvgStyle(this, context);
1745 } else {
1746 SvgStyleWriter::saveSvgFill(this->background(), false, this->outlineRect(), this->size(), this->absoluteTransformation(), context);
1747 SvgStyleWriter::saveSvgStroke(this->stroke(), context);
1749 inheritPaintOrder(), context, true);
1750 }
1751 shapeSpecificStyles = this->shapeTypeSpecificStyles(context);
1752 } else {
1753 if (isTextPath) {
1754 context.shapeWriter().startElement("textPath", false);
1755 } else {
1756 context.shapeWriter().startElement("tspan", false);
1757 }
1758 SvgStyleWriter::saveSvgBasicStyle(it->properties.property(KoSvgTextProperties::Visibility, true).toBool(),
1759 it->properties.property(KoSvgTextProperties::Opacity, 0).toReal(),
1760 it->properties.property(KoSvgTextProperties::PaintOrder,
1761 QVariant::fromValue(paintOrder())
1763 !it->properties.hasProperty(KoSvgTextProperties::PaintOrder), context, true);
1764
1765 }
1766
1767 KoShape *textPath = KoSvgTextShape::Private::textPathByName(it->textPathId, d->textPaths);
1768 success = it->saveSvg(context,
1769 it == d->textData.compositionBegin(),
1770 d->childCount(siblingCurrent(it)) == 0,
1771 shapeSpecificStyles,
1772 textPath);
1773 } else {
1774 if (it == d->textData.compositionBegin()) {
1775 SvgStyleWriter::saveMetadata(this, context);
1776 }
1777 context.shapeWriter().endElement();
1778 }
1779 }
1780 if (writeGroup) {
1781 context.shapeWriter().endElement();
1782 }
1783 return success;
1784}
1785
1787{
1788 bool success = true;
1790 for (auto it = d->textData.compositionBegin(); it != d->textData.compositionEnd(); it++) {
1791 if (it.state() == KisForestDetail::Enter) {
1792 QMap<QString, QString> shapeSpecificStyles;
1793
1794 if (it == d->textData.compositionBegin()) {
1795 context.shapeWriter().startElement("p", false);
1796 } else {
1797 context.shapeWriter().startElement("span", false);
1798 }
1799 KoSvgTextProperties ownProperties = it->properties.ownProperties(parentProps.last(),
1800 it == d->textData.compositionBegin());
1801 parentProps.append(ownProperties);
1802 QMap<QString, QString> attributes = ownProperties.convertToSvgTextAttributes();
1803 if (it == d->textData.compositionBegin())
1804 attributes.insert(ownProperties.convertParagraphProperties());
1805 bool addedFill = false;
1806 if (attributes.size() > 0) {
1807 QString styleString;
1808 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
1809 if (QString(it.key().toLatin1().data()).contains("text-anchor")) {
1810 QString val = it.value();
1811 if (it.value()=="middle") {
1812 val = "center";
1813 } else if (it.value()=="end") {
1814 val = "right";
1815 } else {
1816 val = "left";
1817 }
1818 styleString.append("text-align")
1819 .append(": ")
1820 .append(val)
1821 .append(";" );
1822 } else if (QString(it.key().toLatin1().data()).contains("fill")) {
1823 styleString.append("color")
1824 .append(": ")
1825 .append(it.value())
1826 .append(";" );
1827 addedFill = true;
1828 } else if (QString(it.key().toLatin1().data()).contains("font-size")) {
1829 QString val = it.value();
1830 styleString.append(it.key().toLatin1().data())
1831 .append(": ")
1832 .append(val)
1833 .append(";" );
1834 } else {
1835 styleString.append(it.key().toLatin1().data())
1836 .append(": ")
1837 .append(it.value())
1838 .append(";" );
1839 }
1840 }
1841 if (ownProperties.hasProperty(KoSvgTextProperties::FillId) && !addedFill) {
1842 KoColorBackground *b = dynamic_cast<KoColorBackground *>(it->properties.background().data());
1843 if (b) {
1844 styleString.append("color")
1845 .append(": ")
1846 .append(b->color().name())
1847 .append(";" );
1848 }
1849 }
1850 context.shapeWriter().addAttribute("style", styleString);
1851
1852 if (d->childCount(siblingCurrent(it)) == 0) {
1853 debugFlake << "saveHTML" << this << it->text;
1854 // After adding all the styling to the <p> element, add the text
1855 context.shapeWriter().addTextNode(it->text);
1856 }
1857 }
1858 } else {
1859 parentProps.pop_back();
1860 context.shapeWriter().endElement();
1861 }
1862 }
1863 return success;
1864}
1865
1867{
1868
1869}
1870
1872{
1873
1874}
1875
1877{
1879 d->result,
1880 d->lineBoxes,
1881 d->cursorPos,
1882 d->logicalToVisualCursorPos,
1883 d->plainText,
1884 d->isBidi,
1885 d->initialTextPosition));
1886}
1887
1889{
1890 // TODO: add an assert that all linked shpaes in memento are present in
1891 // the current state of d->textPaths. That is the responsibility of
1892 // KoSvgTextAddRemoveShapeCommandImpl to prepare the shapes for us
1893
1894 KoSvgTextShapeMementoImpl *impl = dynamic_cast<KoSvgTextShapeMementoImpl*>(memento.data());
1895 if (impl) {
1896 d->textData = impl->textData;
1897 d->result = impl->result;
1898 d->lineBoxes = impl->lineBoxes;
1899 d->cursorPos = impl->cursorPos;
1900 d->logicalToVisualCursorPos = impl->logicalToVisualCursorPos;
1901 d->plainText = impl->plainText;
1902 d->isBidi = impl->isBidi;
1903 d->initialTextPosition = impl->initialTextPosition;
1904
1905 // Ensure that any text paths exist.
1906 auto root = d->textData.childBegin();
1907 for (auto child = childBegin(root); child != childEnd(root); child++) {
1908 if (!child->textPathId.isEmpty()) {
1909 KIS_SAFE_ASSERT_RECOVER(Private::textPathByName(child->textPathId, d->textPaths)) {
1910 qDebug() << "missing path is" << child->textPathId;
1911 child->textPathId = QString();
1912 }
1913 }
1914 }
1915 }
1916}
1917
1919{
1920 setMementoImpl(memento);
1921 if (d->bulkActionState) {
1922 d->bulkActionState->layoutSetFromMemento = true;
1923 }
1924 notifyChanged();
1927}
1928
1929void KoSvgTextShape::setMemento(const KoSvgTextShapeMementoSP memento, int pos, int anchor)
1930{
1931 const bool shapeOffsetBefore = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1933 setMementoImpl(memento);
1934 const bool shapeOffsetAfter = (textProperties().hasProperty(KoSvgTextProperties::ShapeMarginId)
1936 if (shapeOffsetBefore || shapeOffsetAfter) {
1937 if (d->bulkActionState) {
1938 d->bulkActionState->contourHasChanged = true;
1939 } else {
1940 d->updateTextWrappingAreas();
1941 }
1942 } else {
1943 if (d->bulkActionState) {
1944 d->bulkActionState->layoutSetFromMemento = true;
1945 }
1946 }
1947 notifyChanged();
1950}
1951
1953{
1954 qDebug() << "Tree size:" << KisForestDetail::size(d->textData);
1955 QString spaces;
1956 for (auto it = compositionBegin(d->textData); it != compositionEnd(d->textData); it++) {
1957 if (it.state() == KisForestDetail::Enter) {
1958
1959 qDebug() << QString(spaces + "+") << it->text;
1960 qDebug() << QString(spaces + "|") << it->properties.convertToSvgTextAttributes();
1961 qDebug() << QString(spaces + "| PropertyType:") << it->properties.property(KoSvgTextProperties::KraTextStyleType).toString();
1962 qDebug() << QString(spaces + "| Fill set: ") << it->properties.hasProperty(KoSvgTextProperties::FillId);
1963 qDebug() << QString(spaces + "| Stroke set: ") << it->properties.hasProperty(KoSvgTextProperties::StrokeId);
1964 qDebug() << QString(spaces + "| Opacity: ") << it->properties.property(KoSvgTextProperties::Opacity);
1965 qDebug() << QString(spaces + "| PaintOrder: ") << it->properties.hasProperty(KoSvgTextProperties::PaintOrder);
1966 qDebug() << QString(spaces + "| Visibility set: ") << it->properties.hasProperty(KoSvgTextProperties::Visibility);
1967 qDebug() << QString(spaces + "| TextPath set: ") << it->textPathId;
1968 qDebug() << QString(spaces + "| Transforms set: ") << it->localTransformations;
1969 spaces.append(" ");
1970 }
1971
1972 if (it.state() == KisForestDetail::Leave) {
1973 spaces.chop(1);
1974 }
1975 }
1976}
1977
1979{
1980 d->isLoading = disable;
1981}
1982
1984{
1985 return d->isLoading;
1986}
1987
1989{
1990 d->disableFontMatching = disable;
1991}
1992
1994{
1995 return d->disableFontMatching;
1996}
1997
1999{
2000 return Private::generateShapes(shapesInside, shapesSubtract, props);
2001}
2002
2004{
2005 KIS_SAFE_ASSERT_RECOVER_RETURN(!d->bulkActionState);
2006 d->bulkActionState.emplace(boundingRect());
2007}
2008
2010{
2012
2013 QRectF updateRect;
2014
2015 if (d->bulkActionState->changed()) {
2016 if (d->bulkActionState->contourHasChanged) {
2017 d->updateTextWrappingAreas();
2018 } else if (d->bulkActionState->layoutHasChanged) {
2019 // updateTextWrappingAreas() already includes a call to relayout()
2020 relayout();
2021 }
2022 // otherwise, it's an update from a memento.
2023
2024 updateRect = d->bulkActionState->originalBoundingRect | boundingRect();
2025 }
2026
2027 d->bulkActionState = std::nullopt;
2028 return updateRect;
2029}
2030
2031void KoSvgTextShape::paint(QPainter &painter) const
2032{
2033 painter.save();
2034
2035 painter.setTransform(d->shapeGroup->absoluteTransformation().inverted()*painter.transform());
2036 d->internalShapesPainter->paint(painter);
2037 painter.restore();
2038
2039 painter.save();
2041 if (textRendering == KoSvgText::RenderingOptimizeSpeed || !painter.testRenderHint(QPainter::Antialiasing)) {
2042 // also apply antialiasing only if antialiasing is active on provided target QPainter
2043 painter.setRenderHint(QPainter::Antialiasing, false);
2044 painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
2045 } else {
2046 painter.setRenderHint(QPainter::Antialiasing, true);
2047 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
2048 }
2049
2050 QPainterPath chunk;
2051 int currentIndex = 0;
2052 if (!d->result.isEmpty()) {
2053 QPainterPath rootBounds;
2054 rootBounds.addRect(Private::boundingBoxFromTree(d->textData, this, false));
2055 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationUnderline, textRendering);
2056 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationOverline, textRendering);
2057 d->paintPaths(painter, rootBounds, this, d->result, textRendering, chunk, currentIndex);
2058 d->paintTextDecoration(painter, rootBounds, this, KoSvgText::DecorationLineThrough, textRendering);
2059 }
2060
2061 painter.restore();
2062}
2063
2064void KoSvgTextShape::paintStroke(QPainter &painter) const
2065{
2066 Q_UNUSED(painter);
2067 // do nothing! everything is painted in paint()
2068}
2069
2070QPainterPath KoSvgTextShape::outline() const {
2071 QPainterPath result;
2072 if (!d->internalShapes().isEmpty()) {
2073 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2074 result.addPath(shape->transformation().map(shape->outline()));
2075 }
2076 }
2077
2078 if ((d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2079 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2080 result.addPath(it->associatedOutline);
2081 for (int i = 0; i < it->textDecorations.values().size(); ++i) {
2082 result.addPath(it->textDecorations.values().at(i));
2083 }
2084 }
2085 }
2086
2087 return result;
2088}
2090{
2091 return outline().boundingRect();
2092}
2093
2095{
2096 QRectF shapesRect;
2097 if (d->internalShapesPainter->contentRect().isValid()) {
2098 shapesRect = d->internalShapesPainter->contentRect();
2099 if (!(d->shapesInside.isEmpty() && d->shapesSubtract.isEmpty())) {
2100 return shapesRect;
2101 }
2102 }
2103 QRectF result = Private::boundingBoxFromTree(d->textData, this, true);
2104
2105 return (this->absoluteTransformation().mapRect(result) | shapesRect);
2106}
2107
2109{
2110 // TODO: check if KoShape::m_d->size is consistent, check cache in KoShapeGroup::size()
2111 return outlineRect().size();
2112}
2113
2114void KoSvgTextShape::setSize(const QSizeF &size)
2115{
2116 const QRectF oRect = this->outlineRect();
2117 const QSizeF oldSize = oRect.size();
2118
2119 if (size == oldSize) return;
2120
2121 // don't try to divide by zero
2122 if (oldSize.isEmpty()) return;
2123
2124 const qreal scaleX = size.width() / oldSize.width();
2125 const qreal scaleY = size.height() / oldSize.height();
2126
2127 if (d->internalShapes().isEmpty()) {
2133 const qreal scaleX = size.width() / oldSize.width();
2134 const qreal scaleY = size.height() / oldSize.height();
2135
2136 this->scale(scaleX, scaleY);
2137 // TODO: use scaling function for kosvgtextproperties when styles presets are merged.
2138 notifyChanged();
2140 } else {
2141 if (!d->textPaths.isEmpty()) return;
2142
2143 const bool allInternalShapeAreTranslatedOnly = [this] () {
2144 Q_FOREACH(KoShape *shape, d->internalShapes()) {
2145 if (shape->transformation().type() > QTransform::TxTranslate) {
2146 return false;
2147 }
2148 }
2149 return true;
2150 }();
2151
2152 if (allInternalShapeAreTranslatedOnly) {
2157 Q_FOREACH (KoShape *internalShape, d->internalShapes()) {
2164 const QPointF stillPoint = this->absoluteTransformation().map(QPointF());
2165 const bool useGlobalMode = false;
2166 const bool usePostScaling = false;
2167 KoFlake::resizeShapeCommon(internalShape,
2168 scaleX,
2169 scaleY,
2170 stillPoint,
2171 useGlobalMode,
2172 usePostScaling,
2173 QTransform());
2174 }
2175
2176 const QSizeF realNewSize = outlineRect().size();
2177 KoShape::setSize(realNewSize);
2178 } else {
2222 const QTransform scale = QTransform::fromScale(scaleX, scaleY);
2223
2224 Q_FOREACH (KoShape *shape, d->internalShapes()) {
2225 shape->setTransformation(shape->transformation() * scale);
2226 }
2227
2228 const QSizeF realNewSize = outlineRect().size();
2229 KoShape::setSize(realNewSize);
2230 }
2231 }
2232}
2233
2234void KoSvgTextShape::paintDebug(QPainter &painter, const DebugElements elements) const
2235{
2236 if (elements & DebugElement::CharBbox) {
2237 int currentIndex = 0;
2238 if (!d->result.isEmpty()) {
2239 QPainterPath rootBounds;
2240 rootBounds.addRect(this->outline().boundingRect());
2241 d->paintDebug(painter, d->result, currentIndex);
2242 }
2243
2244 //Debug shape outlines.
2245 Q_FOREACH (KoShape *shapeInside, d->shapesInside) {
2246 QPainterPath p = shapeInside->outline();
2247 p = shapeInside->transformation().map(p);
2248 painter.strokePath(p, QPen(Qt::green));
2249 }
2250 Q_FOREACH (KoShape *shapeInside, d->shapesSubtract) {
2251 QPainterPath p = shapeInside->outline();
2252 p = shapeInside->transformation().map(p);
2253 painter.strokePath(p, QPen(Qt::red));
2254 }
2255 }
2256
2257 if (elements & DebugElement::LineBox) {
2258 Q_FOREACH (LineBox lineBox, d->lineBoxes) {
2259 Q_FOREACH (const LineChunk &chunk, lineBox.chunks) {
2260 QPen pen;
2261 pen.setCosmetic(true);
2262 pen.setWidth(2);
2263 painter.setBrush(QBrush(Qt::transparent));
2264 pen.setColor(QColor(0, 128, 255, 128));
2265 painter.setPen(pen);
2266 painter.drawLine(chunk.length);
2267 pen.setColor(QColor(255, 128, 0, 128));
2268 painter.setPen(pen);
2269 painter.drawRect(chunk.boundingBox);
2270
2271 pen.setColor(QColor(255, 0, 0, 128));
2272 pen.setStyle(Qt::DashDotDotLine);
2273 painter.setPen(pen);
2274 painter.drawLine(chunk.length.translated(lineBox.baselineTop));
2275 pen.setColor(QColor(0, 128, 0, 128));
2276 pen.setStyle(Qt::DashDotLine);
2277 painter.setPen(pen);
2278 painter.drawLine(chunk.length.translated(lineBox.baselineBottom));
2279 }
2280 }
2281 }
2282}
2283
2285{
2286 KoShape *shape = nullptr;
2287 int currentIndex = 0;
2288 if (!d->result.empty()) {
2289 shape = d->collectPaths(this, d->result, currentIndex);
2290 }
2291
2292 return shape;
2293}
2294
2296{
2297 KoSvgText::AutoValue inlineSize = d->textData.childBegin()->properties.propertyOrDefault(KoSvgTextProperties::InlineSizeId).value<KoSvgText::AutoValue>();
2298 if (!d->shapesInside.isEmpty()) {
2299 return TextType::TextInShape;
2300 } else if (!inlineSize.isAuto) {
2301 return TextType::InlineWrap;
2302 } else {
2303 bool textSpaceCollapse = false;
2304 for (auto it = d->textData.depthFirstTailBegin(); it != d->textData.depthFirstTailEnd(); it++) {
2305 KoSvgText::TextSpaceCollapse collapse = KoSvgText::TextSpaceCollapse(it->properties.propertyOrDefault(KoSvgTextProperties::TextCollapseId).toInt());
2306 if (collapse == KoSvgText::Collapse || collapse == KoSvgText::PreserveSpaces) {
2307 textSpaceCollapse = true;
2308 break;
2309 }
2310 if (!it->localTransformations.isEmpty()) {
2311 textSpaceCollapse = true;
2312 break;
2313 }
2314 }
2316 }
2318}
2319
2321{
2322 removeShapesFromContours(d->shapesInside, false);
2324}
2325
2327{
2328 Q_FOREACH(KoShape *shape, shapes) {
2329 if (d->textPaths.contains(shape)) {
2330 d->removeTextPathId(d->textData.childBegin(), shape->name());
2331 d->cleanUp(d->textData);
2332 d->textPaths.removeAll(shape);
2333 }
2334
2335 if (inside) {
2336 if (d->shapesSubtract.contains(shape)) {
2337 d->shapesSubtract.removeAll(shape);
2338 }
2339 d->shapesInside.append(shape);
2340 } else {
2341 if (d->shapesInside.contains(shape)) {
2342 d->shapesInside.removeAll(shape);
2343 }
2344 d->shapesSubtract.append(shape);
2345 }
2346 if (!d->shapeGroup->shapes().contains(shape)) {
2347 d->shapeGroup->addShape(shape);
2348 shape->addDependee(this);
2349 }
2350 }
2351
2352 notifyChanged(); // notify shape manager that our geometry has changed
2353
2354 if (!d->isLoading) {
2355 d->updateTextWrappingAreas();
2356 }
2357
2358 d->updateInternalShapesList();
2360 update();
2361}
2362
2364{
2365 return (shape->parent() == d->shapeGroup.data());
2366}
2367
2368void KoSvgTextShape::removeShapesFromContours(QList<KoShape *> shapes, bool callUpdate, bool cleanup)
2369{
2370 Q_FOREACH(KoShape *shape, shapes) {
2371 if (shape) {
2372 d->removeTextPathId(d->textData.childBegin(), shape->name());
2373 shape->removeDependee(this);
2374 d->shapesInside.removeAll(shape);
2375 d->shapesSubtract.removeAll(shape);
2376 d->textPaths.removeAll(shape);
2377
2378 }
2379 d->shapeGroup->removeShape(shape);
2380 }
2381 if (cleanup) {
2382 d->cleanUp(d->textData);
2383 }
2384 if (callUpdate) {
2385 notifyChanged(); // notify shape manager that our geometry has changed
2386 if (!d->isLoading) {
2387 d->updateTextWrappingAreas();
2388 }
2389 d->updateInternalShapesList();
2391 update();
2392 }
2393}
2394
2395void KoSvgTextShape::moveShapeInsideToIndex(KoShape *shapeInside, const int index)
2396{
2397 const int oldIndex = d->shapesInside.indexOf(shapeInside);
2398 if (oldIndex < 0) return;
2399
2400 // Update.
2401 d->shapesInside.move(oldIndex, index);
2402 if (!d->isLoading) {
2403 d->updateTextWrappingAreas();
2404 }
2405 d->updateInternalShapesList();
2407 update();
2408}
2409
2410bool KoSvgTextShape::setTextPathOnRange(KoShape *textPath, const int startPos, const int endPos)
2411{
2412 const int finalPos = d->cursorPos.size() - 1;
2413 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->cursorPos.at(qBound(0, startPos, finalPos)).index;
2414 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->cursorPos.at(qBound(0, endPos, finalPos)).index;
2415
2416 Private::splitTree(d->textData, startIndex, false);
2417 Private::splitTree(d->textData, endIndex, true);
2418 int currentIndex = 0;
2419
2420 Private::makeTextPathNameUnique(d->textPaths, textPath);
2421 KoSvgTextContentElement textPathElement;
2422 textPathElement.textPathId = textPath->name();
2423 d->shapeGroup->addShape(textPath);
2424 d->textPaths.append(textPath);
2425 textPath->addDependee(this);
2426
2427
2428 if (KisForestDetail::depth(d->textData) == 1) {
2429 textPathElement.text = d->textData.childBegin()->text;
2433 Q_FOREACH(KoSvgTextProperties::PropertyId p, copyIds) {
2434 if (d->textData.childBegin()->properties.hasProperty(KoSvgTextProperties::TextDecorationLineId)) {
2435 textPathElement.properties.setProperty(p, d->textData.childBegin()->properties.property(p));
2436 d->textData.childBegin()->properties.removeProperty(p);
2437 }
2438 }
2439
2440 d->textData.childBegin()->text = QString();
2441 d->textData.insert(childEnd(d->textData.childBegin()), textPathElement);
2442 } else {
2443 // find nodes
2444 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2445 currentIndex = 0;
2446 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2447
2448 auto first = startElement.node()?Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2449 : childEnd(d->textData.childBegin());
2450 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2451 : childEnd(d->textData.childBegin());
2452
2453 // move children. we collect them before inserting the text path,
2454 // so we don't get iterator issues.
2456 auto textPathIt = textPathTree.insert(
2457 textPathTree.childEnd(),
2458 textPathElement);
2460 for (auto child = first;
2461 (child != last && child != childEnd(d->textData.childBegin()));
2462 child++) {
2463 if (!child->textPathId.isEmpty()) {
2464 Q_FOREACH(KoShape *shape, d->textPaths) {
2465 if (shape->name() == child->textPathId) {
2466 removeShapesFromContours({shape}, false, false);
2467 break;
2468 }
2469 }
2470 child->textPathId = QString();
2471 }
2472 movableChildren.append(child);
2473 }
2474 while (!movableChildren.isEmpty()) {
2475 auto child = movableChildren.takeLast();
2476 textPathTree.move(child, KisForestDetail::childBegin(textPathIt));
2477 }
2478 d->textData.move(textPathIt, last);
2479 }
2480 Private::cleanUp(d->textData);
2481
2482 d->updateInternalShapesList();
2483 notifyChanged();
2485 update();
2486 return true;
2487}
2488
2489QList<KoShape *> KoSvgTextShape::textPathsAtRange(const int startPos, const int endPos)
2490{
2491 const int finalPos = d->cursorPos.size() - 1;
2492 const int startIndex = (startPos == endPos && startPos < 0)? 0: d->cursorPos.at(qBound(0, startPos, finalPos)).index;
2493 const int endIndex = (startPos == endPos && startPos < 0)? finalPos: d->cursorPos.at(qBound(0, endPos, finalPos)).index;
2495 int currentIndex = 0;
2496 auto startElement = Private::findTextContentElementForIndex(d->textData, currentIndex, startIndex, true);
2497 currentIndex = 0;
2498 auto endElement = Private::findTextContentElementForIndex(d->textData, currentIndex, endIndex, true);
2499
2500 auto first = startElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(startElement))
2501 : childEnd(d->textData.childBegin());
2502 auto last = endElement.node()? Private::findTopLevelParent(d->textData.childBegin(), KisForestDetail::siblingCurrent(endElement))
2503 : childEnd(d->textData.childBegin());
2504 if (last != childEnd(d->textData.childBegin())) {
2505 last++;
2506 }
2507 for (auto child = first; (child != last && child != KisForestDetail::siblingEnd(first)); child++) {
2508 if (KoShape *path = Private::textPathByName(child->textPathId, d->textPaths)) {
2509 textPaths.append(path);
2510 }
2511 }
2512 return textPaths;
2513}
2514
2516{
2517 auto root = d->textData.childBegin();
2518 if (root == d->textData.childEnd()) return;
2519 if (d->textPaths.contains(textPath)) return;
2520
2521 Private::makeTextPathNameUnique(d->textPaths, textPath);
2522 KoSvgTextContentElement textPathElement;
2523 textPathElement.textPathId = textPath->name();
2524 d->shapeGroup->addShape(textPath);
2525 d->textPaths.append(textPath);
2526 textPath->addDependee(this);
2527
2528 d->textData.insert(childEnd(root), textPathElement);
2529}
2530
2532{
2533 return d->shapesInside;
2534}
2535
2537{
2538 removeShapesFromContours(d->shapesSubtract, false);
2540}
2541
2543{
2544 return d->currentTextWrappingAreas;
2545}
2546
2548{
2549 return d->shapesSubtract;
2550}
2551
2553{
2554 return d->internalShapesPainter->internalShapeManager();
2555}
2556
2557QMap<QString, QString> KoSvgTextShape::shapeTypeSpecificStyles(SvgSavingContext &context) const
2558{
2559 QMap<QString, QString> map = this->textProperties().convertParagraphProperties();
2560 if (!d->shapesInside.isEmpty()) {
2561 QStringList shapesInsideList;
2562 Q_FOREACH(KoShape* shape, d->shapesInside) {
2563 QString id = (shape->isVisible(false) && !context.strippedTextMode())? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2564 shapesInsideList.append(QString("url(#%1)").arg(id));
2565 }
2566 map.insert("shape-inside", shapesInsideList.join(" "));
2569 map.insert("overflow", "clip");
2570 }
2571 if (!d->shapesSubtract.isEmpty()) {
2572 QStringList shapesInsideList;
2573 Q_FOREACH(KoShape* shape, d->shapesSubtract) {
2574 QString id = shape->isVisible(false)? context.getID(shape): SvgStyleWriter::embedShape(shape, context);
2575 shapesInsideList.append(QString("url(#%1)").arg(id));
2576 }
2577 map.insert("shape-subtract", shapesInsideList.join(" "));
2578 }
2579
2580 return map;
2581}
2582
2584{
2585 d->relayout();
2586}
2587
2589 : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18nc("Text label in SVG Text Tool", "Text"))
2590{
2591 setToolTip(i18n("SVG Text Shape"));
2592 setIconName(koIconNameCStr("x-shape-text"));
2595
2597 t.name = i18n("SVG Text");
2598 t.iconName = koIconName("x-shape-text");
2599 t.toolTip = i18n("SVG Text Shape");
2600 addTemplate(t);
2601}
2602
2604{
2605 Q_UNUSED(documentResources);
2606 debugFlake << "Create default svg text shape";
2607
2608 KoSvgTextShape *shape = new KoSvgTextShape();
2610 shape->insertText(0, i18nc("Default text for the text shape", "Placeholder Text"));
2611
2612 return shape;
2613}
2614
2616{
2617 KoSvgTextShape *shape = new KoSvgTextShape();
2619
2620 QString svgText = params->stringProperty("svgText", i18nc("Default text for the text shape", "<text>Placeholder Text</text>"));
2621 QString defs = params->stringProperty("defs" , "<defs/>");
2622 QRectF shapeRect = QRectF(0, 0, 200, 60);
2623 QVariant rect = params->property("shapeRect");
2624 QVariant origin = params->property("origin");
2625
2626#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2627 if (rect.type()==QVariant::RectF) {
2628#else
2629 if (rect.typeId() == QMetaType::QRectF) {
2630#endif
2631 shapeRect = rect.toRectF();
2632 }
2633
2634 KoSvgTextShapeMarkupConverter converter(shape);
2635 converter.convertFromSvg(svgText,
2636 defs,
2637 shapeRect,
2638 documentResources->documentResolution());
2639#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2640 if (origin.type() == QVariant::PointF) {
2641#else
2642 if (origin.typeId() == QMetaType::QPointF) {
2643#endif
2644 shape->setPosition(origin.toPointF());
2645 } else {
2646 shape->setPosition(shapeRect.topLeft());
2647 }
2649
2650 return shape;
2651}
2652
2653bool KoSvgTextShapeFactory::supports(const QDomElement &/*e*/, KoShapeLoadingContext &/*context*/) const
2654{
2655 return false;
2656}
2657
2659{
2660 Q_UNUSED(type);
2661 Q_UNUSED(shape);
2662}
2663
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)
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.