Krita Source Code Documentation
Loading...
Searching...
No Matches
SvgTextCursor.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2023 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6#include "SvgTextCursor.h"
7#include "KoCanvasBase.h"
15#include "SvgTextShortCuts.h"
16
18#include "KoSvgPaste.h"
19#include "KoColorBackground.h"
20#include "KoShapeStroke.h"
21#include "KoColor.h"
22
23#include "KoViewConverter.h"
25#include "kis_painting_tweaks.h"
26#include "KoCanvasController.h"
31
32#include "kundo2command.h"
33#include <QTimer>
34#include <QDebug>
35#include <QClipboard>
36#include <QMimeData>
37#include <QApplication>
38#include <QKeyEvent>
39#include <QKeySequence>
40#include <QAction>
41#include <kis_assert.h>
42#include <QInputMethodEvent>
43#include <QBuffer>
44#include <QWidget>
45
46#ifdef Q_OS_ANDROID
47#include <config-qt-patches-present.h>
48#endif
49
50
52 int start = -1;
53 int length = 0;
54 KoSvgText::TextDecorations decor = KoSvgText::DecorationNone;
56 bool thick = false;
57
58 void setDecorationFromQStyle(QTextCharFormat::UnderlineStyle s) {
59 // whenever qt sets an underlinestyle it always sets the underline.
60 decor.setFlag(KoSvgText::DecorationUnderline, s != QTextCharFormat::NoUnderline);
61 if (s == QTextCharFormat::DotLine) {
63 } else if (s == QTextCharFormat::DashUnderline) {
65 } else if (s == QTextCharFormat::WaveUnderline) {
67 } else if (s == QTextCharFormat::SpellCheckUnderline) {
69#ifdef Q_OS_MACOS
71#endif
72 } else {
74 }
75 }
76
77 void setDecorationFromQTextCharFormat(QTextCharFormat format) {
78 if (format.hasProperty(QTextFormat::FontUnderline)) {
79 decor.setFlag(KoSvgText::DecorationUnderline, format.property(QTextFormat::FontUnderline).toBool());
80 }
81 if (format.hasProperty(QTextFormat::FontOverline)) {
82 decor.setFlag(KoSvgText::DecorationOverline, format.property(QTextFormat::FontOverline).toBool());
83 }
84 if (format.hasProperty(QTextFormat::FontStrikeOut)) {
85 decor.setFlag(KoSvgText::DecorationLineThrough, format.property(QTextFormat::FontStrikeOut).toBool());
86 }
87
88 if (format.hasProperty(QTextFormat::TextUnderlineStyle)) {
89 setDecorationFromQStyle(format.underlineStyle());
90 }
97 if (format.hasProperty(QTextFormat::BackgroundBrush)) {
98 thick = format.background().isOpaque();
99#ifdef Q_OS_LINUX
100 if (style == KoSvgText::Dashed) {
102 }
103#endif
104
105 }
107 // Ensure a underline is always set.
109 }
110 }
111};
112
114 QPair<QPointF, QPointF> handles;
116
117 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> baselines;
118 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths;
119
120 QPainterPath edges;
121 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentPaths;
122 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentBaselines;
123
125
126 QRectF boundingRect(qreal handleRadius) {
127 QRectF total;
128 for (int i = 0; i< paths.values().size(); i++) {
129 total |= paths.values().at(i).boundingRect();
130 }
131 for (int i = 0; i< baselines.values().size(); i++) {
132 total |= baselines.values().at(i).boundingRect();
133 }
134 for (int i = 0; i< parentPaths.values().size(); i++) {
135 total |= parentPaths.values().at(i).boundingRect();
136 }
137 for (int i = 0; i< parentBaselines.values().size(); i++) {
138 total |= parentBaselines.values().at(i).boundingRect();
139 }
140 total |= edges.boundingRect();
141 QRectF rect(0, 0, handleRadius, handleRadius);
142 rect.moveCenter(handles.first);
143 total |= rect;
144 rect.moveCenter(handles.second);
145 total |= rect;
146 return total;
147 }
148
149 bool testBaselines(Qt::KeyboardModifiers modifiers) {
150 return (modifiers & Qt::ShiftModifier);
151 }
152
153};
154
155struct Q_DECL_HIDDEN SvgTextCursor::Private {
157 {
158 public:
160 : m_d(d)
161 // Only unblock updates if they were blocked to begin with.
162 , m_unblockQueryUpdates(!std::exchange(d->blockQueryUpdates, true))
163 {
164 }
165
166 InputQueryUpdateBlocker(const QScopedPointer<Private> &d)
168 {
169 }
170
172 {
173 if (m_d) {
174 QInputMethod *inputMethod = QGuiApplication::inputMethod();
175
176 if (m_unblockQueryUpdates) {
177 m_d->blockQueryUpdates = false;
178 inputMethod->update(Qt::ImQueryInput);
179 }
180
181 if (m_changeVisibility) {
182 inputMethod->setVisible(m_d->shape != nullptr);
183 }
184 }
185 }
186
187 void setChangeVisibility(bool changeVisibility)
188 {
189 m_changeVisibility = changeVisibility;
190 }
191
192 private:
195 bool m_changeVisibility = false;
196 };
197
199 bool isAddingCommand = false;
200 int pos = 0;
201 int anchor = 0;
202 KoSvgTextShape *shape {nullptr};
203
206 bool cursorVisible = false;
207 bool hasFocus = false;
208
209 QPainterPath cursorShape;
214 int cursorWidth = 1;
215 bool drawCursorInAdditionToSelection = false;
216 QPainterPath selection;
218
219
220 // This is used to adjust cursorpositions better on text-shape relayouts.
221 int posIndex = 0;
222 int anchorIndex = 0;
223
224 bool visualNavigation = true;
225 bool pasteRichText = true;
226
227 Qt::KeyboardModifiers lastKnownModifiers;
228
229 bool typeSettingMode = false;
231 bool drawTypeSettingHandle = true;
232 qreal handleRadius = 7;
235
236 SvgTextInsertCommand *preEditCommand {nullptr};
237 int preEditStart = -1;
238 int preEditLength = -1;
240 QPainterPath IMEDecoration;
242 bool blockQueryUpdates = false;
243
245
247
249};
250
252 d(new Private)
253{
254 d->canvas = canvas;
255 d->interface = new SvgTextCursorPropertyInterface(this);
256 if (d->canvas->canvasController()) {
257 // Mockcanvas in the tests has no canvas controller.
258 connect(d->canvas->canvasController()->proxyObject, SIGNAL(sizeChanged(QSize)), this, SLOT(updateInputMethodItemTransform()));
259 connect(d->canvas->canvasController()->proxyObject,
260 SIGNAL(moveDocumentOffset(QPointF, QPointF)),
261 this,
263 connect(d->canvas->canvasController()->proxyObject,
264 SIGNAL(effectiveZoomChanged(qreal)),
265 this,
267 connect(d->canvas->canvasController()->proxyObject,
268 SIGNAL(documentRotationChanged(qreal)),
269 this,
271 connect(d->canvas->canvasController()->proxyObject,
272 SIGNAL(documentMirrorStatusChanged(bool, bool)),
273 this,
275 d->resourceManagerAcyclicConnector.connectBackwardResourcePair(
276 d->canvas->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)),
277 this, SLOT(canvasResourceChanged(int,QVariant)));
278 d->resourceManagerAcyclicConnector.connectForwardVoid(d->interface, SIGNAL(textCharacterSelectionChanged()), this, SLOT(updateCanvasResources()));
279 }
280
281}
282
284{
286 d->cursorFlash.stop();
287 d->cursorFlashLimit.stop();
288 d->shape = nullptr;
289}
290
292{
293 return d->shape;
294}
295
297{
298 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
299 inputQueryUpdateBlocker.setChangeVisibility(true);
300
301 if (d->shape) {
303 d->shape->removeShapeChangeListener(this);
304 }
305 d->shape = textShape;
306 if (d->shape) {
307 d->shape->addShapeChangeListener(this);
309 d->pos = d->shape->posForIndex(d->shape->plainText().size());
311 } else {
312 d->pos = 0;
313 }
314 d->anchor = 0;
315 updateCursor(true);
317 d->interface->emitSelectionChange();
318}
319
320void SvgTextCursor::setCaretSetting(int cursorWidth, int cursorFlash, int cursorFlashLimit, bool drawCursorInAdditionToSelection)
321{
322 d->cursorFlash.setInterval(cursorFlash/2);
323 d->cursorFlashLimit.setInterval(cursorFlashLimit);
324 d->cursorWidth = cursorWidth;
325 d->drawCursorInAdditionToSelection = drawCursorInAdditionToSelection;
326 connect(&d->cursorFlash, SIGNAL(timeout()), this, SLOT(blinkCursor()));
327 connect(&d->cursorFlashLimit, SIGNAL(timeout()), this, SLOT(stopBlinkCursor()));
328}
329
330void SvgTextCursor::setVisualMode(bool visualMode)
331{
332 d->visualNavigation = visualMode;
333}
334
335void SvgTextCursor::setPasteRichTextByDefault(const bool pasteRichText)
336{
337 d->pasteRichText = pasteRichText;
338}
339
341{
342 d->typeSettingMode = activate;
344}
345
347{
348 return d->pos;
349}
350
352{
353 return d->anchor;
354}
355
356void SvgTextCursor::setPos(int pos, int anchor)
357{
358 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
359 d->pos = pos;
360 d->anchor = anchor;
361 updateCursor();
363}
364
365void SvgTextCursor::setPosToPoint(QPointF point, bool moveAnchor)
366{
367 if (d->shape) {
368 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
369 int pos = d->shape->posForPointLineSensitive(d->shape->documentToShape(point));
370 if (d->preEditCommand) {
371 int start = d->shape->indexForPos(d->preEditStart);
372 int end = start + d->preEditLength;
373 int posIndex = d->shape->indexForPos(pos);
374 if (posIndex > start && posIndex <= end) {
375 qApp->inputMethod()->invokeAction(QInputMethod::Click, posIndex - start);
376 return;
377 } else {
379 }
380 }
381
382 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
383 d->pos = qBound(0, pos, finalPos);
384 if (moveAnchor || d->anchor < 0 || d->anchor > finalPos) {
385 d->anchor = d->pos;
386 }
387 updateCursor();
389 }
390}
391
393{
395
396 if (!(d->typeSettingMode && d->shape && d->canvas)) return handle;
397
398 const QRectF roiInShape = d->shape->absoluteTransformation().inverted().mapRect(regionOfInterest);
399
400 if (d->typeSettingDecor.handlesEnabled) {
401 if (roiInShape.contains(d->typeSettingDecor.handles.first)) {
403 } else if (roiInShape.contains(d->typeSettingDecor.handles.second)) {
404 handle = SvgTextCursor::EndPos;
405 }
406 }
407 if (handle != NoHandle) return handle;
408 if (!d->typeSettingDecor.boundingRect(d->handleRadius).intersects(roiInShape)) return handle;
409
410 qreal closest = std::numeric_limits<qreal>::max();
411 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths
412 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.baselines: d->typeSettingDecor.paths;
413 Q_FOREACH (const SvgTextCursor::TypeSettingModeHandle baseline, paths.keys()) {
414 const QPainterPath path = paths.value(baseline);
415 if (!path.intersects(roiInShape)) continue;
416
417 const QList<QPolygonF> polys = path.toSubpathPolygons();
418 Q_FOREACH(const QPolygonF poly, polys) {
419 if (poly.size() < 2) continue;
420 for (int i = 1; i < poly.size(); i++) {
421 QLineF l(poly.at(i-1), poly.at(i));
422 qreal distance = kisDistanceToLine(roiInShape.center(), l);
423 if (distance < closest) {
424 handle = baseline;
425 closest = distance;
426 d->typeSettingDecor.closestBaselinePoint =
427 kisProjectOnVector(l.p2() - l.p1(), roiInShape.center() - l.p1()) + l.p1();
428 }
429 }
430 }
431 }
432
433 return handle;
434}
435
437{
438 d->hoveredTypeSettingHandle = hovered;
439}
440
442{
443 d->drawTypeSettingHandle = draw;
444}
445
447{
448 if (d->shape) {
449 d->typeSettingDecor.handlesEnabled = ((d->shape->textType() == KoSvgTextShape::PreformattedText
450 || d->shape->textType() == KoSvgTextShape::PrePositionedText)
451 && !(d->shape->topLevelNodeForPos(d->pos).textPath()));
452 }
453}
454
456{
457 if (d->hoveredTypeSettingHandle == StartPos ||
458 d->hoveredTypeSettingHandle == StartPos ||
459 d->typeSettingDecor.testBaselines(d->lastKnownModifiers)) {
460 return Qt::ArrowCursor;
461 } else if (d->shape) {
462 return (d->shape->writingMode() == KoSvgText::HorizontalTB)? Qt::SizeVerCursor: Qt::SizeHorCursor;
463 }
464 return Qt::ArrowCursor;
465}
466
468{
469 bool baseline = d->typeSettingDecor.testBaselines(d->lastKnownModifiers);
470 if (handle == Ascender) {
471 if (baseline) {
472 return i18nc("Type setting mode line name", "Text Top");
473 } else {
474 return i18nc("Type setting mode line name", "Font Size");
475 }
476 } else if (handle == Descender) {
477 if (baseline) {
478 return i18nc("Type setting mode line name", "Text Bottom");
479 } else {
480 return i18nc("Type setting mode line name", "Font Size");
481 }
482 } else if (handle == BaselineAlphabetic) {
483 return i18nc("Type setting mode line name", "Alphabetic");
484 } else if (handle == BaselineIdeographic) {
485 return i18nc("Type setting mode line name", "Ideographic");
486 } else if (handle == BaselineHanging) {
487 return i18nc("Type setting mode line name", "Hanging");
488 } else if (handle == BaselineMiddle) {
489 return i18nc("Type setting mode line name", "Middle");
490 } else if (handle == BaselineMathematical) {
491 return i18nc("Type setting mode line name", "Mathematical");
492 } else if (handle == BaselineCentral) {
493 return i18nc("Type setting mode line name", "Central");
494 } else if (handle == LineHeightTop || handle == LineHeightBottom) {
495 return i18nc("Type setting mode line name", "Line Height");
496 } else if (handle == BaselineShift) {
497 if (baseline) {
498 return i18nc("Type setting mode line name", "Current Baseline");
499 } else {
500 return i18nc("Type setting mode line name", "Baseline Shift");
501 }
502 } else {
503 return QString();
504 }
505}
506
508{
509 if (handle == NoHandle) return false;
510 if (!d->typeSettingDecor.testBaselines(d->lastKnownModifiers)) return false;
512 if (handle == Ascender) {
514 } else if (handle == Descender) {
516 } else if (handle == BaselineAlphabetic) {
518 } else if (handle == BaselineIdeographic) {
520 } else if (handle == BaselineHanging) {
522 } else if (handle == BaselineMiddle) {
523 baseline = KoSvgText::BaselineMiddle;
524 } else if (handle == BaselineMathematical) {
526 } else if (handle == BaselineCentral) {
528 } else {
529 return false;
530 }
532 props.setProperty(KoSvgTextProperties::DominantBaselineId, QVariant::fromValue(baseline));
533 props.setProperty(KoSvgTextProperties::AlignmentBaselineId, QVariant::fromValue(baseline));
535 return true;
536}
537
538QMap<SvgTextCursor::TypeSettingModeHandle, int> typeSettingBaselinesFromMetrics(const KoSvgText::FontMetrics metrics, const qreal lineGap, const bool isHorizontal) {
539 return QMap<SvgTextCursor::TypeSettingModeHandle, int> {
547 {SvgTextCursor::BaselineMiddle, isHorizontal? metrics.xHeight/2: metrics.ideographicCenterBaseline},
548 {SvgTextCursor::LineHeightTop, metrics.ascender+(lineGap/2)},
549 {SvgTextCursor::LineHeightBottom, metrics.descender-(lineGap/2)},
551 };
552}
553
554int SvgTextCursor::posForTypeSettingHandleAndRect(const TypeSettingModeHandle handle, const QRectF regionOfInterest)
555{
556 if (!d->shape) return 0;
557
559 d->shape->getPositionsAndRotationsForRange(d->pos, d->anchor);
560 if (infos.size() < 1) return 0;
561
562 const QRectF roi = d->shape->documentToShape(regionOfInterest);
563
564 for (auto it = infos.begin(); it != infos.end(); it++) {
565 const int currentPos = (d->pos == d->anchor)? -1 :d->shape->posForIndex(it->logicalIndex);
566 const KoSvgTextProperties props = d->shape->propertiesForPos(currentPos, true);
568
569 KoSvgText::FontMetrics metrics = (d->pos == d->anchor)? props.metrics(true, true): it->metrics;
570 const bool isHorizontal = d->shape->writingMode() == KoSvgText::HorizontalTB;
571
572 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
573 const int lineGap = lineHeight.isNormal? metrics.lineGap: (lineHeight.length.value/scaleMetrics)-(metrics.ascender-metrics.descender);
574
575 QTransform t = QTransform::fromTranslate(it->finalPos.x(), it->finalPos.y());
576 t.rotate(it->rotateDeg);
577
578 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
579 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
580
581 const int metric = types.value(handle);
582 QPointF offset = isHorizontal? QPointF(0, -(metric*scaleMetrics)): QPointF(metric*scaleMetrics, 0);
583 QLineF line = t.map(QLineF(offset, offset+it->advance));
584 if (KisAlgebra2D::intersectLineRect(line, roi.toAlignedRect(), false)) return d->shape->posForIndex(it->logicalIndex);
585 }
586
587 return 0;
588}
589
590
591
592void SvgTextCursor::moveCursor(MoveMode mode, bool moveAnchor)
593{
594 if (d->shape) {
595
596 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
597 d->pos = qBound(0, moveModeResult(mode, d->pos, d->visualNavigation), finalPos);
598
599 if (moveAnchor) {
600 d->anchor = d->pos;
601 }
603 updateCursor();
604 }
605}
606
608{
609
610 if (d->shape) {
611 //KUndo2Command *parentCmd = new KUndo2Command;
612 if (hasSelection()) {
613 SvgTextRemoveCommand *removeCmd = removeSelectionImpl(false);
614 addCommandToUndoAdapter(removeCmd);
615 }
616
617 SvgTextInsertCommand *insertCmd = new SvgTextInsertCommand(d->shape, d->pos, d->anchor, text);
618 addCommandToUndoAdapter(insertCmd);
619
620 }
621}
622
623void SvgTextCursor::insertRichText(KoSvgTextShape *insert, bool inheritPropertiesIfPossible)
624{
625 if (d->shape) {
626 //KUndo2Command *parentCmd = new KUndo2Command;
627 if (hasSelection()) {
628 SvgTextRemoveCommand *removeCmd = removeSelectionImpl(false);
629 addCommandToUndoAdapter(removeCmd);
630 }
631
632 SvgTextInsertRichCommand *cmd = new SvgTextInsertRichCommand(d->shape, insert, d->pos, d->anchor, inheritPropertiesIfPossible);
634
635 }
636}
637
639{
640 if (d->shape) {
641 SvgTextRemoveCommand *removeCmd;
642 if (hasSelection()) {
643 removeCmd = removeSelectionImpl(true);
644 addCommandToUndoAdapter(removeCmd);
645 } else {
646 int posA = moveModeResult(first, d->pos, d->visualNavigation);
647 int posB = moveModeResult(second, d->pos, d->visualNavigation);
648
649 int posStart = qMin(posA, posB);
650 int posEnd = qMax(posA, posB);
651 int indexEnd = d->shape->indexForPos(posEnd);
652 int length = indexEnd - d->shape->indexForPos(posStart);
653
654 removeCmd = new SvgTextRemoveCommand(d->shape, indexEnd, d->pos, d->anchor, length, true);
655 addCommandToUndoAdapter(removeCmd);
656 }
657 }
658}
659
661{
662 if (d->shape) {
663 SvgTextRemoveCommand *removeCmd;
664 if (hasSelection()) {
665 removeCmd = removeSelectionImpl(true);
666 addCommandToUndoAdapter(removeCmd);
667 } else {
668 int lastIndex = d->shape->indexForPos(d->pos);
669 removeCmd = new SvgTextRemoveCommand(d->shape, lastIndex, d->pos, d->anchor, 1, true);
670 addCommandToUndoAdapter(removeCmd);
671 }
672 }
673}
674
675QPair<KoSvgTextProperties, KoSvgTextProperties> SvgTextCursor::currentTextProperties() const
676{
677 if (d->shape) {
678 return QPair<KoSvgTextProperties, KoSvgTextProperties>(d->shape->propertiesForPos(d->pos), d->shape->propertiesForPos(d->pos, true));
679 }
680 return QPair<KoSvgTextProperties, KoSvgTextProperties>();
681}
682
684{
685 if (!d->shape) return QList<KoSvgTextProperties>();
686 int start = -1;
687 int end = -1;
688 start = qMin(d->pos, d->anchor);
689 end = qMax(d->pos, d->anchor);
690 return d->shape->propertiesForRange(start, end);
691}
692
694{
695 if (!d->shape) return QList<KoSvgTextProperties>();
696 return {d->shape->propertiesForRange(-1, -1)};
697}
698
699void SvgTextCursor::mergePropertiesIntoSelection(const KoSvgTextProperties props, const QSet<KoSvgTextProperties::PropertyId> removeProperties, bool paragraphOnly, bool selectWord)
700{
701 if (d->shape) {
702 int start = -1;
703 int end = -1;
704 if (!paragraphOnly) {
705 start = d->pos;
706 end = d->anchor;
707 }
708 if (selectWord && d->pos == d->anchor) {
709 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
710 start = qBound(0, moveModeResult(MoveWordStart, d->pos, d->visualNavigation), finalPos);
711 end = qBound(0, moveModeResult(MoveWordEnd, d->pos, d->visualNavigation), finalPos);
712 }
713 KUndo2Command *cmd = new SvgTextMergePropertiesRangeCommand(d->shape, props, start, end, removeProperties);
715 }
716}
717
719{
720 KUndo2Command *removeCmd = removeSelectionImpl(true);
721 addCommandToUndoAdapter(removeCmd);
722}
723
725{
726 SvgTextRemoveCommand *removeCmd = nullptr;
727 if (d->shape) {
728 if (d->anchor != d->pos) {
729 int end = d->shape->indexForPos(qMax(d->anchor, d->pos));
730 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - d->shape->indexForPos(qMin(d->anchor, d->pos));
731 removeCmd = new SvgTextRemoveCommand(d->shape, end, d->pos, d->anchor, length, allowCleanUp, parent);
732 }
733 }
734 return removeCmd;
735}
736
738{
739 if (d->shape) {
740 int start = d->shape->indexForPos(qMin(d->anchor, d->pos));
741 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - start;
742 QString copied = d->shape->plainText().mid(start, length);
743 std::unique_ptr<KoSvgTextShape> copy = d->shape->copyRange(start, length);
744 QClipboard *cb = QApplication::clipboard();
745
746 if (copy) {
747 KoSvgTextShapeMarkupConverter converter(copy.get());
748 QString svg;
749 QString styles;
750 QString html;
751 QMimeData *svgData = new QMimeData();
752 if (converter.convertToSvg(&svg, &styles)) {
753 QString svgDoc = QString("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"2.0\">%1\n%2</svg>").arg(styles).arg(svg);
754 svgData->setData(QLatin1String("image/svg+xml"), svgDoc.toUtf8());
755 }
756 svgData->setText(copied);
757 if (converter.convertToHtml(&html))
758 svgData->setHtml(html);
759 cb->setMimeData(svgData);
760 } else {
761 cb->setText(copied);
762 }
763
764 }
765}
766
768{
769 bool success = d->pasteRichText? pasteRichText(): pastePlainText();
770 return success;
771}
772
774{
775 bool success = false;
776 if (d->shape) {
777 QClipboard *cb = QApplication::clipboard();
778 const QMimeData *mimeData = cb->mimeData();
779 KoSvgPaste shapePaste;
780 if (shapePaste.hasShapes()) {
781 QList<KoShape*> shapes = shapePaste.fetchShapes(d->shape->boundingRect(), 72.0);
782 while (shapes.size() > 0) {
783 KoSvgTextShape *textShape = dynamic_cast<KoSvgTextShape*>(shapes.takeFirst());
784 if (textShape) {
785 insertRichText(textShape);
786 success = true;
787 }
788 }
789 } else if (mimeData->hasHtml()) {
790 QString html = mimeData->html();
791 KoSvgTextShape *insert = new KoSvgTextShape();
792 KoSvgTextShapeMarkupConverter converter(insert);
793 QString svg;
794 QString styles;
795 if (converter.convertFromHtml(html, &svg, &styles)
796 && converter.convertFromSvg(svg, styles, d->shape->boundingRect(), 72.0) ) {
797 insertRichText(insert);
798 success = true;
799 }
800 }
801
802 if (!success) {
803 success = pastePlainText();
804 }
805 }
806 return success;
807}
808
810{
811 bool success = false;
812 QClipboard *cb = QApplication::clipboard();
813 const QMimeData *mimeData = cb->mimeData();
814 if (mimeData->hasText()) {
815 insertText(mimeData->text());
816 success = true;
817 }
818 return success;
819}
820
822{
823 if (d->shape) {
824 KUndo2Command *cmd = new SvgTextRemoveTransformsFromRange(d->shape, d->pos, d->anchor);
826 }
827}
828
830{
831 setPos(d->pos, d->pos);
832}
833
834static QColor bgColorForCaret(QColor c, int opacity = 64) {
835
836 return KisPaintingTweaks::luminosityCoarse(c) > 0.8? QColor(0, 0, 0, opacity) : QColor(255, 255, 255, opacity);
837}
838
839void SvgTextCursor::paintDecorations(QPainter &gc, QColor selectionColor, int decorationThickness, qreal handleRadius)
840{
841 if (d->shape) {
842 gc.save();
843 gc.setTransform(d->shape->absoluteTransformation(), true);
844
845 if (d->pos != d->anchor && !d->typeSettingMode) {
846 gc.save();
847 gc.setOpacity(0.5);
848 QBrush brush(selectionColor);
849 gc.fillPath(d->selection, brush);
850 gc.restore();
851 }
852
853 if ( (d->drawCursorInAdditionToSelection || d->pos == d->anchor)
854 && d->cursorVisible) {
855 QPen pen;
856 pen.setCosmetic(true);
857 QColor c = d->cursorColor.isValid()? d->cursorColor: Qt::black;
858 pen.setColor(bgColorForCaret(c));
859 pen.setWidth((d->cursorWidth + 2) * decorationThickness);
860 gc.setPen(pen);
861 gc.drawPath(d->cursorShape);
862 pen.setColor(c);
863 pen.setWidth(d->cursorWidth * decorationThickness);
864 gc.setPen(pen);
865 gc.drawPath(d->cursorShape);
866
867 }
868
869 if (d->preEditCommand) {
870 gc.save();
871 QBrush brush(selectionColor);
872 gc.setOpacity(0.5);
873 gc.fillPath(d->IMEDecoration, brush);
874 gc.restore();
875 }
876 if (d->typeSettingMode && d->drawTypeSettingHandle) {
877 d->handleRadius = handleRadius;
878 QTransform painterTf = gc.transform();
879 KisHandlePainterHelper helper(&gc, handleRadius, decorationThickness);
882
883 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths
884 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.baselines: d->typeSettingDecor.paths;
885 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentPaths
886 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.parentBaselines: d->typeSettingDecor.parentPaths;
887 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, paths.keys()) {
888 const QPainterPath p = paths.value(handle);
889 const QPainterPath parent = parentPaths.value(handle);
890 if (d->hoveredTypeSettingHandle == handle) {
891 helper.setHandleStyle(highlight);
892 helper.drawPath(parent);
893 helper.drawPath(p);
894 } else {
895 gc.save();
896 QPen pen(selectionColor, decorationThickness, handle == BaselineShift? Qt::SolidLine: Qt::DashLine);
897 pen.setCosmetic(true);
898 gc.setPen(pen);
899 gc.setOpacity(0.5);
900 gc.drawPath(painterTf.map(parent));
901 gc.drawPath(painterTf.map(p));
902 gc.restore();
903 }
904 gc.save();
905 QPen pen(selectionColor, decorationThickness, Qt::SolidLine);
906 pen.setCosmetic(true);
907 gc.setPen(pen);
908 gc.setOpacity(0.5);
909 gc.drawPath(painterTf.map(d->typeSettingDecor.edges));
910 gc.restore();
911 }
912
913 if (d->typeSettingDecor.handlesEnabled) {
914 helper.setHandleStyle(d->hoveredTypeSettingHandle == EndPos? highlight: regular);
915 helper.drawHandleCircle(d->typeSettingDecor.handles.second);
916 helper.setHandleStyle(d->hoveredTypeSettingHandle == StartPos? highlight: regular);
917 helper.drawHandleRect(d->typeSettingDecor.handles.first);
918 }
919 QString name = handleName(d->hoveredTypeSettingHandle);
920 if (!name.isEmpty()) {
921 QPainterPath textP;
922 // When we're drawing on opengl, there's no anti-aliasing, so we should have full hinting for readabiltiy.
923 QFont font = gc.font();
924 font.setHintingPreference(QFont::PreferFullHinting);
925 textP.addText(painterTf.map(d->typeSettingDecor.closestBaselinePoint).toPoint(), font, name);
926 gc.save();
927 QPen pen(bgColorForCaret(selectionColor, 255));
928 pen.setCosmetic(true);
929 pen.setWidth(decorationThickness);
930 gc.setPen(pen);
931 gc.drawPath(textP);
932 gc.fillPath(textP, QBrush(selectionColor));
933 gc.restore();
934 }
935
936 }
937 gc.restore();
938 }
939}
940
941QVariant SvgTextCursor::inputMethodQuery(Qt::InputMethodQuery query) const
942{
943 dbgTools << "receiving inputmethod query" << query;
944
945 // Because we set the input item transform to be shape->document->view->widget->window,
946 // the coordinates here should be in shape coordinates.
947 switch(query) {
948 case Qt::ImEnabled:
949 return d->shape? true: false;
950 break;
951 case Qt::ImCursorRectangle:
952 // The platform integration will always define the cursor as the 'left side' handle.
953 if (d->shape) {
954 QPointF caret1(d->cursorCaret.p1());
955 QPointF caret2(d->cursorCaret.p2());
956
957
958 QRectF rect = QRectF(caret1, caret2).normalized();
959 if (!rect.isValid()) {
960 if (rect.height() < 1) {
961 rect.adjust(0, -1, 0, 0);
962 }
963 if (rect.width() < 1) {
964 rect.adjust(0, 0, 1, 0);
965 }
966
967 }
968 return rect.toAlignedRect();
969 }
970 break;
971 case Qt::ImAnchorRectangle:
972 // The platform integration will always define the anchor as the 'right side' handle.
973 if (d->shape) {
974 QPointF caret1(d->anchorCaret.p1());
975 QPointF caret2(d->anchorCaret.p2());
976 QRectF rect = QRectF(caret1, caret2).normalized();
977 if (rect.isEmpty()) {
978 if (rect.height() < 1) {
979 rect.adjust(0, -1, 0, 0);
980 }
981 if (rect.width() < 1) {
982 rect = rect.adjusted(-1, 0, 0, 0).normalized();
983 }
984 }
985 return rect.toAlignedRect();
986 }
987 break;
988 //case Qt::ImFont: // not sure what this is used for, but we cannot sent out without access to properties.
989 case Qt::ImAbsolutePosition:
990 case Qt::ImCursorPosition:
991 if (d->shape) {
992 return d->shape->indexForPos(d->pos);
993 }
994 break;
995 case Qt::ImSurroundingText:
996 if (d->shape) {
997 QString surroundingText = d->shape->plainText();
998 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
999 surroundingText.remove(preEditIndex, d->preEditLength);
1000 return surroundingText;
1001 }
1002 break;
1003 case Qt::ImCurrentSelection:
1004 if (d->shape) {
1005 QString surroundingText = d->shape->plainText();
1006 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1007 surroundingText.remove(preEditIndex, d->preEditLength);
1008 int start = d->shape->indexForPos(qMin(d->anchor, d->pos));
1009 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - start;
1010 return surroundingText.mid(start, length);
1011 }
1012 break;
1013 case Qt::ImTextBeforeCursor:
1014 if (d->shape) {
1015 int start = d->shape->indexForPos(d->pos);
1016 QString surroundingText = d->shape->plainText();
1017 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1018 surroundingText.remove(preEditIndex, d->preEditLength);
1019 return surroundingText.left(start);
1020 }
1021 break;
1022 case Qt::ImTextAfterCursor:
1023 if (d->shape) {
1024 int start = d->shape->indexForPos(d->pos);
1025 QString surroundingText = d->shape->plainText();
1026 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1027 surroundingText.remove(preEditIndex, d->preEditLength);
1028 return surroundingText.right(start);
1029 }
1030 break;
1031 case Qt::ImMaximumTextLength:
1032 return QVariant(); // infinite text length!
1033 break;
1034 case Qt::ImAnchorPosition:
1035 if (d->shape) {
1036 return d->shape->indexForPos(d->anchor);
1037 }
1038 break;
1039 case Qt::ImHints:
1040 // It would be great to use Qt::ImhNoTextHandles or Qt::ImhNoEditMenu,
1041 // but neither are implemented for anything but web platform integration
1042 return Qt::ImhMultiLine;
1043 break;
1044 // case Qt::ImPreferredLanguage: // requires access to properties.
1045#if defined(Q_OS_ANDROID) && KRITA_QT_HAS_ANDROID_INPUT_PLATFORM_DATA_SOFT_INPUT_ADJUST_NOTHING
1046 case Qt::ImPlatformData:
1047 // Platform-specific data. Qt normally only uses this on iOS, but we
1048 // have a patch that allows us to control the keyboard pan behavior.
1049 // Normally it pans the application window up if the text area would end
1050 // up underneath the keyboard, but that's silly for the on-canvas text
1051 // input because the location can be changed by panning the canvas. And
1052 // if you do that while the keyboard is up, you end up with the whole
1053 // window panned up for no reason until you dismiss and re-show it, so
1054 // better to just do nothing in the first place and let the user pan.
1055 return Qt::ANDROID_INPUT_PLATFORM_DATA_SOFT_INPUT_ADJUST_NOTHING;
1056#endif
1057 case Qt::ImEnterKeyType:
1058 if (d->shape) {
1059 return Qt::EnterKeyDefault; // because input method hint is always multiline, this will show a return key.
1060 }
1061 break;
1062 // case Qt::ImInputItemClipRectangle // whether the input item is clipped?
1063 default:
1064 return QVariant();
1065 }
1066 return QVariant();
1067}
1068
1069void SvgTextCursor::inputMethodEvent(QInputMethodEvent *event)
1070{
1071 dbgTools << "Commit:"<< event->commitString() << "predit:"<< event->preeditString();
1072 dbgTools << "Replacement:"<< event->replacementStart() << event->replacementLength();
1073
1074 QRectF updateRect = d->shape? d->shape->boundingRect(): QRectF();
1075 SvgTextShapeManagerBlocker blocker(d->canvas->shapeManager());
1076
1077 bool isGettingInput = !event->commitString().isEmpty() || !event->preeditString().isEmpty()
1078 || event->replacementLength() > 0;
1079
1080 // Remove previous preedit string.
1081 if (d->preEditCommand) {
1082 d->preEditCommand->undo();
1083 d->preEditCommand = 0;
1084 d->preEditStart = -1;
1085 d->preEditLength = -1;
1086 updateRect |= d->shape? d->shape->boundingRect(): QRectF();
1087 }
1088
1089 if (!d->shape || !isGettingInput) {
1090 blocker.unlock();
1091 d->canvas->shapeManager()->update(updateRect);
1092 event->ignore();
1093 return;
1094 }
1095
1096 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1097
1098 // remove the selection if any.
1100
1101 // set the text insertion pos to replacement start and also remove replacement length, if any.
1102 int originalPos = d->pos;
1103 int index = d->shape->indexForPos(d->pos) + event->replacementStart();
1104 d->pos = d->shape->posForIndex(index);
1105 if (event->replacementLength() > 0) {
1107 index + event->replacementLength(),
1108 originalPos,
1109 d->anchor,
1110 event->replacementLength(),
1111 false);
1113 }
1114
1115 // add the commit string, if any.
1116 if (!event->commitString().isEmpty()) {
1117 insertText(event->commitString());
1118 }
1119
1120 // set the selection...
1121 Q_FOREACH(const QInputMethodEvent::Attribute attribute, event->attributes()) {
1122 if (attribute.type == QInputMethodEvent::Selection) {
1123 d->pos = d->shape->posForIndex(attribute.start);
1124 int index = d->shape->indexForPos(d->pos);
1125 d->anchor = d->shape->posForIndex(index + attribute.length);
1126 }
1127 }
1128
1129
1130 // insert a preedit string, if any.
1131 if (!event->preeditString().isEmpty()) {
1132 int index = d->shape->indexForPos(d->pos);
1133 d->preEditCommand = new SvgTextInsertCommand(d->shape, d->pos, d->anchor, event->preeditString());
1134 d->preEditCommand->redo();
1135 d->preEditLength = event->preeditString().size();
1136 d->preEditStart = d->shape->posForIndex(index, true);
1137 } else {
1138 d->preEditCommand = 0;
1139 }
1140
1141 // Apply the cursor offset for the preedit.
1143 Q_FOREACH(const QInputMethodEvent::Attribute attribute, event->attributes()) {
1144 dbgTools << "attribute: "<< attribute.type << "start: " << attribute.start
1145 << "length: " << attribute.length << "val: " << attribute.value;
1146 // Text Format is about setting the look of the preedit string, and there can be multiple per event
1147 // we primarily interpret the underline. When a background color is set, we increase the underline
1148 // thickness, as that's what is actually supposed to happen according to the comments in the
1149 // platform input contexts for both macOS and Windows.
1150
1151 if (attribute.type == QInputMethodEvent::TextFormat) {
1152 QVariant val = attribute.value;
1153 QTextCharFormat form = val.value<QTextFormat>().toCharFormat();
1154
1155 if (attribute.length == 0 || attribute.start < 0 || !attribute.value.isValid()) {
1156 continue;
1157 }
1158
1159 int positionA = -1;
1160 int positionB = -1;
1161 if (!styleMap.isEmpty()) {
1162 for (int i = 0; i < styleMap.size(); i++) {
1163 if (attribute.start >= styleMap.at(i).start
1164 && attribute.start < styleMap.at(i).start + styleMap.at(i).length) {
1165 positionA = i;
1166 }
1167 if (attribute.start + attribute.length > styleMap.at(i).start
1168 && attribute.start + attribute.length <= styleMap.at(i).start + styleMap.at(i).length) {
1169 positionB = i;
1170 }
1171 }
1172
1173 if (positionA > -1 && positionA == positionB) {
1174 IMEDecorationInfo decoration1 = styleMap.at(positionA);
1175 IMEDecorationInfo decoration2 = decoration1;
1176 IMEDecorationInfo decoration3 = decoration1;
1177 decoration3.start = (attribute.start+attribute.length);
1178 decoration3.length = (decoration1.start + decoration1.length) - decoration3.start;
1179 decoration1.length = attribute.start - decoration1.start;
1180 decoration2.start = attribute.start;
1181 decoration2.length = attribute.length;
1182 if (decoration1.length > 0) {
1183 styleMap[positionA] = decoration1;
1184 if (decoration2.length > 0) {
1185 positionA += 1;
1186 styleMap.insert(positionA, decoration2);
1187 }
1188 } else {
1189 styleMap[positionA] = decoration2;
1190 }
1191 if (decoration3.length > 0) {
1192 styleMap.insert(positionA + 1, decoration3);
1193 }
1194 } else if (positionA > -1 && positionB > -1
1195 && positionA != positionB) {
1196 IMEDecorationInfo decoration1 = styleMap.at(positionA);
1197 IMEDecorationInfo decoration2 = decoration1;
1198 IMEDecorationInfo decoration3 = styleMap.at(positionB);
1199 IMEDecorationInfo decoration4 = decoration3;
1200 decoration2.length = (decoration1.start + decoration1.length) - attribute.start;
1201 decoration1.length = attribute.start - decoration1.start;
1202 decoration2.start = attribute.start;
1203
1204 decoration4.start = (attribute.start+attribute.length);
1205 decoration3.length = (decoration3.start + decoration3.length) - decoration4.start;
1206 decoration3.length = decoration4.start - decoration3.start;
1207 if (decoration1.length > 0) {
1208 styleMap[positionA] = decoration1;
1209 if (decoration2.length > 0) {
1210 positionA += 1;
1211 styleMap.insert(positionA, decoration2);
1212 }
1213 } else {
1214 styleMap[positionA] = decoration2;
1215 }
1216
1217 if (decoration3.length > 0) {
1218 styleMap[positionB] = decoration3;
1219 if (decoration4.length > 0) {
1220 styleMap.insert(positionB + 1, decoration4);
1221 }
1222 } else {
1223 styleMap[positionB] = decoration4;
1224 }
1225 }
1226 }
1227
1228 if (positionA > -1 && !styleMap.isEmpty()) {
1229
1230 for(int i = positionA; i <= positionB; i++) {
1231 IMEDecorationInfo decoration = styleMap.at(i);
1232 decoration.setDecorationFromQTextCharFormat(form);
1233 styleMap[i] = decoration;
1234 }
1235
1236 } else {
1237 IMEDecorationInfo decoration;
1238 decoration.start = attribute.start;
1239 decoration.length = attribute.length;
1240 decoration.setDecorationFromQTextCharFormat(form);
1241 styleMap.append(decoration);
1242 }
1243
1244 // QInputMethodEvent::Language is about setting the locale on the given preedit string, which is not possible yet.
1245 // QInputMethodEvent::Ruby is supposedly ruby info for the preedit string, but none of the platform integrations
1246 // actually implement this at time of writing, and it may have been something from a previous live of Qt's.
1247 } else if (attribute.type == QInputMethodEvent::Cursor) {
1248 if (d->preEditStart < 0) {
1249 d->anchor = d->pos;
1250 } else {
1251 int index = d->shape->indexForPos(d->preEditStart);
1252 d->pos = d->shape->posForIndex(index + attribute.start);
1253 d->anchor = d->pos;
1254 }
1255
1256 // attribute value is the cursor color, and should be used to paint the cursor.
1257 // attribute length is about whether the cursor should be visible at all...
1258 }
1259 }
1260
1261 blocker.unlock();
1262 updateRect |= d->shape->boundingRect();
1263 // TODO: replace with KoShapeBulkActionLock
1264 d->shape->updateAbsolute(updateRect);
1265 d->styleMap = styleMap;
1268 updateCursor();
1269 event->accept();
1270}
1271
1273{
1274 if (d->shape) {
1275 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->cursorShape.boundingRect()) | d->oldCursorRect);
1276 d->cursorVisible = !d->cursorVisible;
1277 }
1278}
1279
1281{
1282 d->cursorFlash.stop();
1283 d->cursorFlashLimit.stop();
1284 d->cursorVisible = true;
1285 if (d->shape) {
1286 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->cursorShape.boundingRect()) | d->oldCursorRect);
1287 }
1288}
1289
1291{
1292 // Mockcanvas in the tests has no window.
1293 if (!d->canvas->canvasWidget()) {
1294 return;
1295 }
1296 QPoint pos = d->canvas->canvasWidget()->mapTo(d->canvas->canvasWidget()->window(), QPoint());
1297 QTransform widgetToWindow = QTransform::fromTranslate(pos.x(), pos.y());
1298 QTransform inputItemTransform = widgetToWindow;
1299 QRectF inputRect = d->canvas->canvasWidget()->geometry();
1300 if (d->shape) {
1301 inputRect = d->shape->outlineRect().normalized();
1302 QTransform shapeTransform = d->shape->absoluteTransformation();
1303 QTransform docToView = d->canvas->viewConverter()->documentToView();
1304 QTransform viewToWidget = d->canvas->viewConverter()->viewToWidget();
1305 inputItemTransform = shapeTransform * docToView * viewToWidget * widgetToWindow;
1306 // Only mess with IME if we're actually the thing being typed at.
1307 if (d->hasFocus) {
1308 QInputMethod *inputMethod = QGuiApplication::inputMethod();
1309 inputMethod->setInputItemTransform(inputItemTransform);
1310 inputMethod->setInputItemRectangle(inputRect);
1311 if (!d->blockQueryUpdates) {
1312 inputMethod->update(Qt::ImQueryInput);
1313 }
1314 }
1315 }
1316}
1317
1318void SvgTextCursor::canvasResourceChanged(int key, const QVariant &value)
1319{
1321 return;
1322
1323 KoSvgTextProperties props;
1324 KoSvgTextProperties shapeProps = hasSelection()? d->shape->propertiesForPos(qMin(d->pos, d->anchor), true): d->shape->textProperties();
1327 if (!bg->compareTo(shapeProps.background().data())
1328 || !shapeProps.hasProperty(KoSvgTextProperties::FillId)) {
1330 QVariant::fromValue(KoSvgText::BackgroundProperty(bg)));
1331 }
1332 } else if (key == KoCanvasResource::BackgroundColor) {
1335 KoShapeStrokeSP shapeStroke = qSharedPointerDynamicCast<KoShapeStroke>(shapeProps.stroke());
1336 if (shapeStroke->isVisible()) {
1337 stroke.reset(new KoShapeStroke(*shapeStroke));
1338 }
1339 }
1340 stroke->setColor(value.value<KoColor>().toQColor());
1341 if (!stroke->compareFillTo(shapeProps.stroke().data()) || !shapeProps.hasProperty(KoSvgTextProperties::StrokeId)) {
1343 QVariant::fromValue(KoSvgText::StrokeProperty(stroke)));
1344 }
1345 }
1346 if (!props.isEmpty()) {
1347 mergePropertiesIntoSelection(props, QSet<KoSvgTextProperties::PropertyId>(), !hasSelection());
1348 }
1349}
1350
1352{
1353 QAction *action = dynamic_cast<QAction*>(QObject::sender());
1354 if (!action || !d->shape) return;
1355
1356 const QList<KoSvgTextProperties> p = d->shape->propertiesForRange(qMin(d->pos, d->anchor), qMax(d->pos, d->anchor));
1358 if (properties.isEmpty()) return;
1359 mergePropertiesIntoSelection(properties);
1360}
1361
1363{
1364 KoSvgTextProperties props;
1365
1366 QSet<KoSvgTextProperties::PropertyId> ids;
1367
1368 for (int i = 0; i < int(KoSvgTextProperties::LastPropertyId); i++) {
1369 ids.insert(KoSvgTextProperties::PropertyId(i));
1370 }
1371
1372 mergePropertiesIntoSelection(props, ids);
1373}
1374
1376{
1377 return d->pos != d->anchor;
1378}
1379
1381{
1382 Q_UNUSED(type);
1383 Q_UNUSED(shape);
1384 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1385 d->pos = d->shape->posForIndex(d->posIndex);
1386 d->anchor = d->shape->posForIndex(d->anchorIndex);
1387 updateCursor(true);
1391}
1392
1394{
1395 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1396 d->pos = pos;
1397 d->anchor = anchor;
1398 updateCursor();
1401}
1402
1404{
1405 d->interface->emitSelectionChange();
1406 d->interface->emitCharacterSelectionChange();
1407 updateCursor();
1410}
1411
1412void SvgTextCursor::keyPressEvent(QKeyEvent *event)
1413{
1415
1416 updateModifiers(event->modifiers());
1417
1418 if (d->preEditCommand) {
1419 //MacOS will keep sending keyboard events during IME handling.
1420 event->accept();
1421 return;
1422 }
1423
1424 bool select = event->modifiers().testFlag(Qt::ShiftModifier);
1425
1426 if (!((Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier) & event->modifiers())) {
1427
1428 switch (event->key()) {
1429 case Qt::Key_Right:
1431 event->accept();
1432 break;
1433 case Qt::Key_Left:
1435 event->accept();
1436 break;
1437 case Qt::Key_Up:
1439 event->accept();
1440 break;
1441 case Qt::Key_Down:
1443 event->accept();
1444 break;
1445 case Qt::Key_Delete:
1447 event->accept();
1448 break;
1449 case Qt::Key_Backspace:
1451 event->accept();
1452 break;
1453 case Qt::Key_Return:
1454 case Qt::Key_Enter:
1455 insertText("\n");
1456 event->accept();
1457 break;
1458 default:
1459 event->ignore();
1460 }
1461
1462 if (event->isAccepted()) {
1463 return;
1464 }
1465 }
1466 if (acceptableInput(event)) {
1467 insertText(event->text());
1468 event->accept();
1469 return;
1470 }
1471
1472 KoSvgTextProperties props = d->shape->textProperties();
1473
1476
1477 // Qt's keysequence stuff doesn't handle vertical, so to test all the standard keyboard shortcuts as if it did,
1478 // we reinterpret the direction keys according to direction and writing mode, and test against that.
1479
1480 int newKey = event->key();
1481
1482 if (direction == KoSvgText::DirectionRightToLeft) {
1483 switch (newKey) {
1484 case Qt::Key_Left:
1485 newKey = Qt::Key_Right;
1486 break;
1487 case Qt::Key_Right:
1488 newKey = Qt::Key_Left;
1489 break;
1490 default:
1491 break;
1492 }
1493 }
1494
1495 if (mode == KoSvgText::VerticalRL) {
1496 switch (newKey) {
1497 case Qt::Key_Left:
1498 newKey = Qt::Key_Down;
1499 break;
1500 case Qt::Key_Right:
1501 newKey = Qt::Key_Up;
1502 break;
1503 case Qt::Key_Up:
1504 newKey = Qt::Key_Left;
1505 break;
1506 case Qt::Key_Down:
1507 newKey = Qt::Key_Right;
1508 break;
1509 default:
1510 break;
1511 }
1512 } else if (mode == KoSvgText::VerticalRL) {
1513 switch (newKey) {
1514 case Qt::Key_Left:
1515 newKey = Qt::Key_Up;
1516 break;
1517 case Qt::Key_Right:
1518 newKey = Qt::Key_Down;
1519 break;
1520 case Qt::Key_Up:
1521 newKey = Qt::Key_Left;
1522 break;
1523 case Qt::Key_Down:
1524 newKey = Qt::Key_Right;
1525 break;
1526 default:
1527 break;
1528 }
1529 }
1530
1531 QKeySequence testSequence(event->modifiers() | newKey);
1532
1533
1534 // Note for future, when we have format changing actions:
1535 // We'll need to test format change actions before the standard
1536 // keys, as one of the standard keys for deleting a line is ctrl+u
1537 // which would probably be expected to do underline before deleting.
1538
1539 Q_FOREACH(QAction *action, d->actions) {
1540 if (action->shortcut() == testSequence) {
1541 event->accept();
1542 action->trigger();
1543 return;
1544 }
1545 }
1546
1547 // This first set is already tested above, however, if they still
1548 // match, then it's one of the extra sequences for MacOs, which
1549 // seem to be purely logical, instead of the visual set we tested
1550 // above.
1551 if (testSequence == QKeySequence::MoveToNextChar) {
1553 event->accept();
1554 } else if (testSequence == QKeySequence::SelectNextChar) {
1556 event->accept();
1557 } else if (testSequence == QKeySequence::MoveToPreviousChar) {
1559 event->accept();
1560 } else if (testSequence == QKeySequence::SelectPreviousChar) {
1562 event->accept();
1563 } else if (testSequence == QKeySequence::MoveToNextLine) {
1565 event->accept();
1566 } else if (testSequence == QKeySequence::SelectNextLine) {
1568 event->accept();
1569 } else if (testSequence == QKeySequence::MoveToPreviousLine) {
1571 event->accept();
1572 } else if (testSequence == QKeySequence::SelectPreviousLine) {
1574 event->accept();
1575
1576 } else if (testSequence == QKeySequence::MoveToNextWord) {
1578 event->accept();
1579 } else if (testSequence == QKeySequence::SelectNextWord) {
1581 event->accept();
1582 } else if (testSequence == QKeySequence::MoveToPreviousWord) {
1584 event->accept();
1585 } else if (testSequence == QKeySequence::SelectPreviousWord) {
1587 event->accept();
1588
1589 } else if (testSequence == QKeySequence::MoveToStartOfLine) {
1591 event->accept();
1592 } else if (testSequence == QKeySequence::SelectStartOfLine) {
1594 event->accept();
1595 } else if (testSequence == QKeySequence::MoveToEndOfLine) {
1597 event->accept();
1598 } else if (testSequence == QKeySequence::SelectEndOfLine) {
1600 event->accept();
1601
1602 } else if (testSequence == QKeySequence::MoveToStartOfBlock
1603 || testSequence == QKeySequence::MoveToStartOfDocument) {
1605 event->accept();
1606 } else if (testSequence == QKeySequence::SelectStartOfBlock
1607 || testSequence == QKeySequence::SelectStartOfDocument) {
1609 event->accept();
1610
1611 } else if (testSequence == QKeySequence::MoveToEndOfBlock
1612 || testSequence == QKeySequence::MoveToEndOfDocument) {
1614 event->accept();
1615 } else if (testSequence == QKeySequence::SelectEndOfBlock
1616 || testSequence == QKeySequence::SelectEndOfDocument) {
1618 event->accept();
1619
1620 }else if (testSequence == QKeySequence::DeleteStartOfWord) {
1622 event->accept();
1623 } else if (testSequence == QKeySequence::DeleteEndOfWord) {
1625 event->accept();
1626 } else if (testSequence == QKeySequence::DeleteEndOfLine) {
1628 event->accept();
1629 } else if (testSequence == QKeySequence::DeleteCompleteLine) {
1631 event->accept();
1632 } else if (testSequence == QKeySequence::Backspace) {
1634 event->accept();
1635 } else if (testSequence == QKeySequence::Delete) {
1637 event->accept();
1638
1639 } else if (testSequence == QKeySequence::InsertLineSeparator
1640 || testSequence == QKeySequence::InsertParagraphSeparator) {
1641 insertText("\n");
1642 event->accept();
1643 } else {
1644 event->ignore();
1645 }
1646}
1647
1648void SvgTextCursor::updateModifiers(const Qt::KeyboardModifiers modifiers)
1649{
1650 d->lastKnownModifiers = modifiers;
1652}
1653
1655{
1656 return d->isAddingCommand;
1657}
1658
1660{
1661 d->cursorFlash.start();
1662 d->cursorFlashLimit.start();
1663 d->cursorVisible = false;
1664 d->hasFocus = true;
1665 blinkCursor();
1666}
1667
1669{
1670 d->hasFocus = false;
1672}
1673
1674bool SvgTextCursor::registerPropertyAction(QAction *action, const QString &name)
1675{
1676 if (SvgTextShortCuts::configureAction(action, name)) {
1677 d->actions.append(action);
1678 connect(action, SIGNAL(triggered(bool)), this, SLOT(propertyAction()));
1679 return true;
1680 } else if (name == "svg_insert_special_character") {
1681 d->actions.append(action);
1682 connect(action, SIGNAL(triggered(bool)), this, SIGNAL(sigOpenGlyphPalette()));
1683 return true;
1684 } else if (name == "svg_paste_rich_text") {
1685 d->actions.append(action);
1686 connect(action, SIGNAL(triggered(bool)), this, SLOT(pasteRichText()));
1687 return true;
1688 } else if (name == "svg_paste_plain_text") {
1689 d->actions.append(action);
1690 connect(action, SIGNAL(triggered(bool)), this, SLOT(pastePlainText()));
1691 return true;
1692 } else if (name == "svg_remove_transforms_from_range") {
1693 d->actions.append(action);
1694 connect(action, SIGNAL(triggered(bool)), this, SLOT(removeTransformsFromRange()));
1695 return true;
1696 } else if (name == "svg_clear_formatting") {
1697 d->actions.append(action);
1698 connect(action, SIGNAL(triggered(bool)), this, SLOT(clearFormattingAction()));
1699 return true;
1700 } else if (action) {
1701 d->actions.append(action);
1702 return true;
1703 }
1704 return false;
1705}
1706
1711
1712void SvgTextCursor::updateCursor(bool firstUpdate)
1713{
1714 if (d->shape) {
1715 d->oldCursorRect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1716 d->posIndex = d->shape->indexForPos(d->pos);
1717 d->anchorIndex = d->shape->indexForPos(d->anchor);
1718 emit selectionChanged();
1720 }
1721 d->cursorColor = QColor();
1722 d->cursorShape = d->shape? d->shape->cursorForPos(d->pos, d->cursorCaret, d->cursorColor): QPainterPath();
1723
1724 if (!d->blockQueryUpdates) {
1725 qApp->inputMethod()->update(Qt::ImQueryInput);
1726 }
1727 d->interface->emitCharacterSelectionChange();
1728 if (!(d->canvas->canvasWidget() && d->canvas->canvasController())) {
1729 // Mockcanvas in the tests has neither.
1730 return;
1731 }
1732 if (d->shape && !firstUpdate) {
1733 QRectF rect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1734 d->canvas->canvasController()->ensureVisibleDoc(rect, false);
1735 }
1736 if (d->canvas->canvasWidget()->hasFocus()) {
1737 d->cursorFlash.start();
1738 d->cursorFlashLimit.start();
1739 d->cursorVisible = false;
1740 blinkCursor();
1741 }
1742}
1743
1745{
1746 if (d->shape) {
1747 d->oldSelectionRect = d->shape->shapeToDocument(d->selection.boundingRect());
1748 d->shape->cursorForPos(d->anchor, d->anchorCaret, d->cursorColor);
1749 d->selection = d->shape->selectionBoxes(d->pos, d->anchor);
1750 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->selection.boundingRect()) | d->oldSelectionRect);
1751
1752 if (!d->blockQueryUpdates) {
1753 QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
1754 }
1755 }
1756}
1757
1759{
1760 if (d->shape) {
1761 d->oldIMEDecorationRect = d->shape->shapeToDocument(d->IMEDecoration.boundingRect());
1762 KoSvgText::TextDecorations decor;
1763 decor.setFlag(KoSvgText::DecorationUnderline, true);
1764 d->IMEDecoration = QPainterPath();
1765 if (d->preEditCommand) {
1766 Q_FOREACH(const IMEDecorationInfo info, d->styleMap) {
1767
1768 int startIndex = d->shape->indexForPos(d->preEditStart) + info.start;
1769 int endIndex = startIndex + info.length;
1770 qreal minimum = d->canvas->viewToDocument(QPointF(1, 1)).x();
1771 d->IMEDecoration.addPath(d->shape->underlines(d->shape->posForIndex(startIndex),
1772 d->shape->posForIndex(endIndex),
1773 info.decor,
1774 info.style,
1775 minimum,
1776 info.thick));
1777 d->IMEDecoration.setFillRule(Qt::WindingFill);
1778 }
1779 }
1780
1781 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->IMEDecoration.boundingRect()) | d->oldIMEDecorationRect);
1782 }
1783}
1784
1787int calcLineHeight(const KoSvgText::LineHeightInfo &lineHeight, const KoSvgText::FontMetrics &metrics, const qreal scaleMetrics) {
1788 if (!lineHeight.isNormal) {
1789 if (lineHeight.isNumber) {
1790 return (lineHeight.value * (metrics.ascender-metrics.descender))-(metrics.ascender-metrics.descender);
1791 } else {
1792 return (lineHeight.length.value/scaleMetrics)-(metrics.ascender-metrics.descender);
1793 }
1794 }
1795 return metrics.lineGap;
1796}
1797
1799 const int metric, const bool isHorizontal,
1800 QTransform t,
1801 const qreal scaleMetrics, const QPointF &advance,
1802 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> &decor){
1803 QPointF offset = isHorizontal? QPointF(0, -(metric*scaleMetrics)): QPointF(metric*scaleMetrics, 0);
1804
1805 QPainterPath p = decor.value(handle);
1806
1807 const QPointF startPos = t.map(offset);
1808 const QPointF endPos = t.map(offset+advance);
1809 if (p.currentPosition() != startPos) {
1810 p.moveTo(startPos);
1811 }
1812 p.lineTo(endPos);
1813
1814 decor.insert(handle, p);
1815}
1816
1817void processEdges(QTransform t, QMap<SvgTextCursor::TypeSettingModeHandle, int> values,
1818 const bool isHorizontal,
1819 const qreal scaleMetrics,
1820 const QPointF advance,
1821 QPainterPath &path) {
1822 QPointF p1(values.first(), 0);
1823 QPointF p2(values.last(), 0);
1824 Q_FOREACH(const int val, values) {
1825 if (val < p1.x()) {
1826 p1.setX(val);
1827 }
1828 if (val > p2.x()) {
1829 p2.setX(val);
1830 }
1831 }
1832 if (isHorizontal) {
1833 p1 = QPointF(p1.y(), -p1.x());
1834 p2 = QPointF(p2.y(), -p2.x());
1835 }
1836 p1 *= scaleMetrics;
1837 p2 *= scaleMetrics;
1838 path.moveTo(t.map(p1+advance));
1839 path.lineTo(t.map(p2+advance));
1840}
1841
1842QTransform posAndRotateTransform(const QPointF pos, const qreal rotateDeg) {
1843 QTransform t = QTransform::fromTranslate(pos.x(), pos.y());
1844 t.rotate(rotateDeg);
1845 return t;
1846}
1847
1849{
1850 QRectF updateRect;
1851 if (d->shape && d->typeSettingMode) {
1852
1854 d->shape->getPositionsAndRotationsForRange(d->pos, d->anchor);
1855 if (infos.size() < 1) return;
1856
1857 const bool rtl = infos.first().rtl;
1858
1859 KoSvgTextCharacterInfo first = infos.first();
1860 KoSvgTextCharacterInfo last = infos.last();
1861 if (infos.size() > 1) {
1862 std::sort(infos.begin(), infos.end(), KoSvgTextCharacterInfo::visualLessThan);
1863 for (auto it = infos.begin(); it != infos.end(); it++) {
1864 if (it->visualIndex >= 0) {
1865 first = *it;
1866 break;
1867 }
1868 }
1869 for (auto it = infos.rbegin(); it != infos.rend(); it++) {
1870 if (it->visualIndex >= 0) {
1871 last = *it;
1872 break;
1873 }
1874 }
1875 }
1876
1877 QTransform t = QTransform::fromTranslate(last.finalPos.x(), last.finalPos.y());
1878 t.rotate(last.rotateDeg);
1879 last.finalPos = t.map(last.advance);
1880
1881 d->typeSettingDecor.handles.first = rtl? last.finalPos: first.finalPos;
1882 d->typeSettingDecor.handles.second = rtl? first.finalPos: last.finalPos;
1883
1884 //Start collecting the metrics decoration...
1885 d->typeSettingDecor.paths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1886 d->typeSettingDecor.baselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1887 d->typeSettingDecor.edges = QPainterPath();
1888 d->typeSettingDecor.parentPaths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1889 d->typeSettingDecor.parentBaselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1890 QList<KoSvgTextCharacterInfo> metricInfos = infos;
1891 if (d->pos == d->anchor) {
1892 metricInfos = d->shape->getPositionsAndRotationsForRange(0, d->shape->posForIndex(d->shape->plainText().size()));
1893 }
1894 const bool isHorizontal = d->shape->writingMode() == KoSvgText::HorizontalTB;
1895 const int minPos = qMin(d->pos, d->anchor);
1896 const int maxPos = qMax(d->pos, d->anchor);
1897 const int endPos = d->shape->posForIndex(d->shape->plainText().size());
1898
1900 if (minPos > 0 || maxPos < endPos) {
1901 const KoSvgTextProperties props = d->shape->textProperties();
1903 KoSvgText::FontMetrics metrics = props.metrics(true, true);
1904 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1905 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1906
1907 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1908 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1909
1911 QList<int> positions;
1912 positions << 0;
1913 positions << qMax(0, minPos-1);
1914 positions << maxPos;
1915 positions << endPos;
1916
1917 Q_FOREACH(const int pos, positions) {
1919 d->shape->getPositionsAndRotationsForRange(pos, pos);
1920 parentInfos.append(info.first());
1921 }
1922
1923 QPointF drawOffset = isHorizontal? QPointF(d->handleRadius*2, 0): QPointF(0, d->handleRadius*2);
1924 if (d->canvas) {
1925 drawOffset = d->canvas->viewConverter()->viewToDocument().map(drawOffset);
1926 }
1927 bool toggleOffset = rtl;
1928
1929 for (auto it = parentInfos.begin(); it != parentInfos.end(); it++) {
1930 const bool currentIsMin = (d->shape->posForIndex(it->logicalIndex) == minPos);
1931 const bool currentIsMax = (d->shape->posForIndex(it->logicalIndex) == maxPos);
1932
1933 if ((currentIsMin && minPos == 0) || (currentIsMax && maxPos == endPos)) {
1934 toggleOffset = !toggleOffset;
1935 continue;
1936 }
1937
1938 const QPointF finalPos = toggleOffset? it->finalPos + (it->advance - drawOffset): it->finalPos;
1939 const QPointF advance = drawOffset;
1940
1941 const QTransform t = posAndRotateTransform(finalPos, it->rotateDeg);
1942
1943 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1944 const int metric = types.value(handle);
1945 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, advance, d->typeSettingDecor.parentBaselines);
1946 }
1947
1948 {
1949 if (currentIsMin && minPos == maxPos && !toggleOffset) {
1950 toggleOffset = !toggleOffset;
1951 continue;
1952 }
1953 const QPointF advance = toggleOffset? it->advance: QPointF();
1954 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1955 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1956 }
1957
1958 toggleOffset = !toggleOffset;
1959 }
1960 }
1961
1963 bool toggleOffset = false; // MetricInfos are visually-ordered.
1964 for (auto it = metricInfos.begin(); it != metricInfos.end(); it++) {
1965 const int currentPos = (d->pos == d->anchor)? -1 :d->shape->posForIndex(it->logicalIndex);
1966
1967 KoSvgTextProperties props = d->shape->propertiesForPos(currentPos, true);
1969 KoSvgText::FontMetrics propMetrics = props.metrics(true, true);
1970
1971 KoSvgText::FontMetrics metrics = (d->pos == d->anchor)? propMetrics :it->metrics;
1972
1973 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1974 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1975
1976 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1977
1978 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1979 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1980
1981 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1982 const int metric = types.value(handle);
1983 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, it->advance, d->typeSettingDecor.baselines);
1984 }
1985 if ((currentPos == minPos || currentPos+1 == maxPos) && minPos != maxPos) {
1986 const QPointF advance = toggleOffset? it->advance: QPointF();
1987 toggleOffset = !toggleOffset;
1988 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1989 }
1990 }
1991
1993 const QList<SvgTextCursor::TypeSettingModeHandle> nonBaselines = {
1995 };
1996 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, nonBaselines) {
1997 d->typeSettingDecor.paths.insert(handle, d->typeSettingDecor.baselines.value(handle));
1998 d->typeSettingDecor.parentPaths.insert(handle, d->typeSettingDecor.parentBaselines.value(handle));
1999 }
2000 d->typeSettingDecor.baselines.remove(LineHeightTop);
2001 d->typeSettingDecor.baselines.remove(LineHeightBottom);
2002 d->typeSettingDecor.parentBaselines.remove(LineHeightTop);
2003 d->typeSettingDecor.parentBaselines.remove(LineHeightBottom);
2004
2005 updateRect = d->shape->shapeToDocument(d->typeSettingDecor.boundingRect(d->handleRadius));
2006 }
2007 updateTypeSettingDecorFromShape(); // To remove the text-in-path nodes..
2008 Q_EMIT updateCursorDecoration(updateRect | d->oldTypeSettingRect);
2009 d->oldTypeSettingRect = updateRect;
2010}
2011
2013{
2014 if (d->canvas) {
2015 if (cmd) {
2016 d->isAddingCommand = true;
2017 d->canvas->addCommand(cmd);
2018 d->isAddingCommand = false;
2019 }
2020 }
2021}
2022
2023int SvgTextCursor::moveModeResult(const SvgTextCursor::MoveMode mode, int &pos, bool visual) const
2024{
2025 int newPos = pos;
2026 switch (mode) {
2027 case MoveNone:
2028 break;
2029 case MoveLeft:
2030 newPos = d->shape->posLeft(pos, visual);
2031 break;
2032 case MoveRight:
2033 newPos = d->shape->posRight(pos, visual);
2034 break;
2035 case MoveUp:
2036 newPos = d->shape->posUp(pos, visual);
2037 break;
2038 case MoveDown:
2039 newPos = d->shape->posDown(pos, visual);
2040 break;
2041 case MovePreviousChar:
2042 newPos = d->shape->previousIndex(pos);
2043 break;
2044 case MoveNextChar:
2045 newPos = d->shape->nextIndex(pos);
2046 break;
2047 case MovePreviousLine:
2048 newPos = d->shape->previousLine(pos);
2049 break;
2050 case MoveNextLine:
2051 newPos = d->shape->nextLine(pos);
2052 break;
2053 case MoveWordLeft:
2054 newPos = d->shape->wordLeft(pos, visual);
2055 if (newPos == pos) {
2056 newPos = d->shape->posLeft(pos, visual);
2057 newPos = d->shape->wordLeft(newPos, visual);
2058 }
2059 break;
2060 case MoveWordRight:
2061 newPos = d->shape->wordRight(pos, visual);
2062 if (newPos == pos) {
2063 newPos = d->shape->posRight(pos, visual);
2064 newPos = d->shape->wordRight(newPos, visual);
2065 }
2066 break;
2067 case MoveWordStart:
2068 newPos = d->shape->wordStart(pos);
2069 if (newPos == pos) {
2070 newPos = d->shape->previousIndex(pos);
2071 newPos = d->shape->wordStart(newPos);
2072 }
2073 break;
2074 case MoveWordEnd:
2075 newPos = d->shape->wordEnd(pos);
2076 if (newPos == pos) {
2077 newPos = d->shape->nextIndex(pos);
2078 newPos = d->shape->wordEnd(newPos);
2079 }
2080 break;
2081 case MoveLineStart:
2082 newPos = d->shape->lineStart(pos);
2083 break;
2084 case MoveLineEnd:
2085 newPos = d->shape->lineEnd(pos);
2086 break;
2087 case ParagraphStart:
2088 newPos = 0;
2089 break;
2090 case ParagraphEnd:
2091 newPos = d->shape->posForIndex(d->shape->plainText().size());
2092 break;
2093 }
2094 return newPos;
2095}
2096
2098bool SvgTextCursor::acceptableInput(const QKeyEvent *event) const
2099{
2100 const QString text = event->text();
2101 if (text.isEmpty())
2102 return false;
2103 const QChar c = text.at(0);
2104 // Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the
2105 // next test, since CTRL+SHIFT is sometimes used to input it on Windows.
2106 if (c.category() == QChar::Other_Format)
2107 return true;
2108 // QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards
2109 if (event->modifiers() == Qt::ControlModifier
2110 || event->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
2111 return false;
2112 }
2113 if (c.isPrint())
2114 return true;
2115 if (c.category() == QChar::Other_PrivateUse)
2116 return true;
2117 if (c == QLatin1Char('\t'))
2118 return true;
2119 return false;
2120}
2121
2123{
2124 if (!d->preEditCommand) {
2125 return;
2126 }
2127
2128 qApp->inputMethod()->commit();
2129
2130 if (!d->preEditCommand) {
2131 return;
2132 }
2133
2134 d->preEditCommand->undo();
2135 d->preEditCommand = nullptr;
2136 d->preEditStart = -1;
2137 d->preEditLength = 0;
2139 updateCursor();
2140}
2141
2143{
2144 // Only update canvas resources when there's no selection.
2145 // This relies on Krita not setting anything on the text when there's no selection.
2146 if (d->shape && d->canvas->resourceManager() && d->pos == d->anchor) {
2147 KoSvgTextProperties props = hasSelection()? d->shape->propertiesForPos(qMin(d->pos, d->anchor), true): d->shape->textProperties();
2148 KoColorBackground *bg = dynamic_cast<KoColorBackground *>(props.background().data());
2149 if (bg && props.hasProperty(KoSvgTextProperties::FillId)) {
2150 KoColor c;
2151 c.fromQColor(bg->color());
2152 c.setOpacity(1.0);
2153 if (c != d->canvas->resourceManager()->foregroundColor()) {
2154 d->canvas->resourceManager()->setForegroundColor(c);
2155 }
2156 }
2157 KoShapeStroke *stroke = dynamic_cast<KoShapeStroke *>(props.stroke().data());
2158 if (stroke && stroke->isVisible() && stroke->color().isValid() && props.hasProperty(KoSvgTextProperties::StrokeId)) {
2159 KoColor c;
2160 c.fromQColor(stroke->color());
2161 c.setOpacity(1.0);
2162 if (c != d->canvas->resourceManager()->backgroundColor()) {
2163 d->canvas->resourceManager()->setBackgroundColor(c);
2164 }
2165 }
2166
2167 Q_FOREACH (QAction *action, d->actions) {
2168 // Blocking signals so that we don't get a toggle action while evaluating the checked-ness.
2169 action->blockSignals(true);
2170 const QList<KoSvgTextProperties> r = d->shape->propertiesForRange(qMin(d->pos, d->anchor), qMax(d->pos, d->anchor), true);
2171 if (action->isCheckable() && SvgTextShortCuts::possibleActions().contains(action->objectName())) {
2172 const bool checked = SvgTextShortCuts::actionEnabled(action, r);
2173 if (action->isChecked() != checked) {
2174 action->setChecked(checked);
2175 }
2176 }
2177 action->blockSignals(false);
2178 }
2179 }
2180}
2181
2191
2193 : KoSvgTextPropertiesInterface(parent), d(new Private(parent))
2194{
2195 connect(&d->compressor, SIGNAL(timeout()), this, SIGNAL(textSelectionChanged()));
2196 connect(&d->characterCompressor, SIGNAL(timeout()), this, SIGNAL(textCharacterSelectionChanged()));
2197}
2198
2204{
2205 return d->parent->propertiesForShape();
2206}
2207
2209{
2210 // When there's only a single node, its best to only return empty properties, as paragraph properties handle that single node.
2211 if (!d->parent->shape()) return QList<KoSvgTextProperties>();
2212 if (d->parent->shape()->singleNode()) {
2213 return {KoSvgTextProperties()};
2214 }
2215 return d->parent->propertiesForRange();
2216}
2217
2219{
2220 // 9 times out of 10 this is correct, though we could do better by actually
2221 // getting inherited properties for the range and not just defaulting to the paragraph.
2222 return (d->parent->shape())? d->parent->shape()->textProperties(): KoSvgTextProperties();
2223}
2224
2225void SvgTextCursorPropertyInterface::setPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2226{
2227 d->parent->mergePropertiesIntoSelection(properties, removeProperties, true);
2228}
2229
2230void SvgTextCursorPropertyInterface::setCharacterPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2231{
2232 d->parent->mergePropertiesIntoSelection(properties, removeProperties, false, true);
2233}
2234
2236{
2237 return d->parent->hasSelection();
2238}
2239
2244
2246{
2247 // Don't bother updating the selection when there's no shape
2248 // this is so we can use the text properties last used to create new texts.
2249 if (!d->parent->shape()) return;
2250 d->compressor.start();
2251}
2252
2254{
2255 d->characterCompressor.start();
2256}
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
const Params2D p
QPointF p2
QPointF p1
VertexDescriptor get(PredecessorMap const &m, VertexDescriptor v)
qreal distance(const QPointF &p1, const QPointF &p2)
static QColor bgColorForCaret(QColor c, int opacity=64)
QTransform posAndRotateTransform(const QPointF pos, const qreal rotateDeg)
QMap< SvgTextCursor::TypeSettingModeHandle, int > typeSettingBaselinesFromMetrics(const KoSvgText::FontMetrics metrics, const qreal lineGap, const bool isHorizontal)
int calcLineHeight(const KoSvgText::LineHeightInfo &lineHeight, const KoSvgText::FontMetrics &metrics, const qreal scaleMetrics)
void processBaseline(const SvgTextCursor::TypeSettingModeHandle handle, const int metric, const bool isHorizontal, QTransform t, const qreal scaleMetrics, const QPointF &advance, QMap< SvgTextCursor::TypeSettingModeHandle, QPainterPath > &decor)
void processEdges(QTransform t, QMap< SvgTextCursor::TypeSettingModeHandle, int > values, const bool isHorizontal, const qreal scaleMetrics, const QPointF advance, QPainterPath &path)
The KisHandlePainterHelper class is a special helper for painting handles around objects....
void drawPath(const QPainterPath &path)
void drawHandleRect(const QPointF &center, qreal radius)
void setHandleStyle(const KisHandleStyle &style)
void drawHandleCircle(const QPointF &center, qreal radius)
static KisHandleStyle & partiallyHighlightedPrimaryHandles()
static KisHandleStyle & secondarySelection()
A simple solid color shape background.
QColor color() const
Returns the background color.
void setOpacity(quint8 alpha)
Definition KoColor.cpp:333
void fromQColor(const QColor &c)
Convenient function for converting from a QColor.
Definition KoColor.cpp:213
void toQColor(QColor *c) const
a convenience method for the above.
Definition KoColor.cpp:198
bool isVisible() const override
void addShapeChangeListener(ShapeChangeListener *listener)
Definition KoShape.cpp:1152
KoShapeAnchor * anchor() const
ChangeType
Used by shapeChanged() to select which change was made.
Definition KoShape.h:92
QList< KoShape * > fetchShapes(QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize=nullptr)
bool hasShapes()
The KoSvgTextPropertiesInterface class.
void textSelectionChanged()
Emit to signal to KisTextPropertiesManager to call getSelectedProperties.
@ DominantBaselineId
KoSvgText::Baseline.
@ AlignmentBaselineId
KoSvgText::Baseline.
@ LineHeightId
KoSvgText::AutoValue.
@ StrokeId
KoSvgText::StrokeProperty.
@ FillId
KoSvgText::BackgroundProperty.
@ WritingModeId
KoSvgText::WritingMode.
@ DirectionId
KoSvgText::Direction.
@ LastPropertyId
Last Property ID, used for iteration.
QSharedPointer< KoShapeBackground > background() const
KoShapeStrokeModelSP stroke() const
KoSvgText::FontMetrics metrics(const bool withResolvedLineHeight=true, const bool offsetByBaseline=false) const
metrics Return the metrics of the first available font.
bool hasProperty(PropertyId id) const
void setProperty(PropertyId id, const QVariant &value)
QVariant propertyOrDefault(PropertyId id) const
KoSvgText::CssLengthPercentage fontSize() const
bool convertToSvg(QString *svgText, QString *stylesText)
bool convertToHtml(QString *htmlText)
convertToHtml convert the text in the text shape to html
@ PreformattedText
Text-on-Path falls under this or PrePositionedText depending on collapse of lines.
Interface to interact with the text property manager.
const QScopedPointer< Private > d
virtual bool spanSelection() override
Whether the tool is currently selecting a set of characters instead of whole paragraphs.
SvgTextCursorPropertyInterface(SvgTextCursor *parent)
virtual void setCharacterPropertiesOnSelected(KoSvgTextProperties properties, QSet< KoSvgTextProperties::PropertyId > removeProperties=QSet< KoSvgTextProperties::PropertyId >()) override
setCharacterPropertiesOnSelected This sets the properties for a character selection instead of the fu...
virtual KoSvgTextProperties getInheritedProperties() override
getInheritedProperties The properties that should be visible when a given property isn't available in...
virtual QList< KoSvgTextProperties > getSelectedProperties() override
getSelectedProperties
virtual void setPropertiesOnSelected(KoSvgTextProperties properties, QSet< KoSvgTextProperties::PropertyId > removeProperties=QSet< KoSvgTextProperties::PropertyId >()) override
setPropertiesOnSelected This sets the properties on the selection. The implementation is responsible ...
virtual bool characterPropertiesEnabled() override
Whether character selections are possible at all.
virtual QList< KoSvgTextProperties > getCharacterProperties() override
getSelectedProperties
InputQueryUpdateBlocker(SvgTextCursor::Private *d)
void setChangeVisibility(bool changeVisibility)
InputQueryUpdateBlocker(const QScopedPointer< Private > &d)
The SvgTextMergePropertiesRangeCommand class This sets properties on a specific range in a single tex...
The SvgTextRemoveTransformsFromRange class Removes the SVG 1.1 character transforms from the range.
static bool actionEnabled(QAction *action, const QList< KoSvgTextProperties > currentProperties)
static QStringList possibleActions()
static KoSvgTextProperties getModifiedProperties(const QAction *action, QList< KoSvgTextProperties > currentProperties)
static bool configureAction(QAction *action, const QString &name)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define dbgTools
Definition kis_debug.h:48
qreal kisDistanceToLine(const QPointF &m, const QLineF &line)
Definition kis_global.h:234
QPointF kisProjectOnVector(const QPointF &base, const QPointF &v)
Definition kis_global.h:280
bool intersectLineRect(QLineF &line, const QRect rect, bool extend)
qreal luminosityCoarse(const QColor &c, bool sRGBtrc)
luminosityCoarse This calculates the luminosity of the given QColor. It uses a very coarse (10 step) ...
@ BackgroundColor
The active background color selected for this canvas.
@ ForegroundColor
The active foreground color selected for this canvas.
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
@ Wavy
Draw a wavy line. We currently make a zigzag, ex: ^^^^^.
Definition KoSvgText.h:270
@ Dotted
Draw a dotted line. Ex: .....
Definition KoSvgText.h:268
@ DecorationOverline
Definition KoSvgText.h:260
@ DecorationLineThrough
Definition KoSvgText.h:261
@ DecorationNone
Definition KoSvgText.h:258
@ DecorationUnderline
Definition KoSvgText.h:259
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionRightToLeft
Definition KoSvgText.h:50
Baseline
Baseline values used by dominant-baseline and baseline-align.
Definition KoSvgText.h:213
@ BaselineAlphabetic
Use 'romn' or the baseline for LCG scripts.
Definition KoSvgText.h:225
@ BaselineHanging
Definition KoSvgText.h:226
@ BaselineMiddle
Definition KoSvgText.h:232
@ BaselineTextBottom
Bottom side of the inline line-box.
Definition KoSvgText.h:234
@ BaselineIdeographic
Definition KoSvgText.h:223
@ BaselineMathematical
Definition KoSvgText.h:228
@ BaselineTextTop
Top side of the inline line-box.
Definition KoSvgText.h:235
@ BaselineCentral
Use the center between the ideographic over and under.
Definition KoSvgText.h:231
@ HorizontalTB
Definition KoSvgText.h:38
int start
The startPos from the attribute.
void setDecorationFromQTextCharFormat(QTextCharFormat format)
void setDecorationFromQStyle(QTextCharFormat::UnderlineStyle s)
bool thick
Whether the decoration needs to be doubled in size.
int length
The length from the attribute.
KoSvgText::TextDecorationStyle style
The style.
KoSvgText::TextDecorations decor
Which sides get decorated.
The KoSvgTextCharacterInfo class This is a small struct to convey information about character positio...
static bool visualLessThan(const KoSvgTextCharacterInfo &a, const KoSvgTextCharacterInfo &b)
BackgroundProperty is a special wrapper around KoShapeBackground for managing it in KoSvgTextProperti...
Definition KoSvgText.h:714
The FontMetrics class A class to keep track of a variety of font metrics. Note that values are in Fre...
Definition KoSvgText.h:327
qint32 ideographicCenterBaseline
default baseline for vertical, centered between over and under.
Definition KoSvgText.h:347
qint32 ideographicUnderBaseline
location of ideographic under baseline from origin, may fall back to descender.
Definition KoSvgText.h:346
qint32 xHeight
height of X, defaults to 0.5 fontsize.
Definition KoSvgText.h:334
qint32 lineGap
additional linegap between consecutive lines.
Definition KoSvgText.h:341
qint32 fontSize
Currently set size, CSS unit 'em'.
Definition KoSvgText.h:329
qint32 alphabeticBaseline
location of alphabetic baseline from origin.
Definition KoSvgText.h:343
qint32 descender
distance for origin to bottom.
Definition KoSvgText.h:340
qint32 ascender
distance from origin to top.
Definition KoSvgText.h:339
qint32 mathematicalBaseline
location of mathematical baseline from origin.
Definition KoSvgText.h:344
qint32 hangingBaseline
location of the hanging baseline used in north brahmic scripts.
Definition KoSvgText.h:354
bool isNumber
Length or number.
Definition KoSvgText.h:695
CssLengthPercentage length
Definition KoSvgText.h:693
bool isNormal
It's a number indicating the lineHeight;.
Definition KoSvgText.h:696
StrokeProperty is a special wrapper around KoShapeStrokeModel for managing it in KoSvgTextProperties.
Definition KoSvgText.h:733
Private(KisCanvas2 *c)
The SvgTextCursor class.
void setTypeSettingHandleHovered(TypeSettingModeHandle hovered=TypeSettingModeHandle::NoHandle)
Set a given typesetting handle as hovered, so it will be drawn as such.
void setVisualMode(const bool visualMode=true)
setVisualMode set whether the navigation mode is visual or logical. This right now primarily affects ...
void keyPressEvent(QKeyEvent *event)
Handle the cursor-related key events.
KoSvgTextPropertiesInterface * textPropertyInterface()
void mergePropertiesIntoSelection(const KoSvgTextProperties props, const QSet< KoSvgTextProperties::PropertyId > removeProperties=QSet< KoSvgTextProperties::PropertyId >(), bool paragraphOnly=false, bool selectWord=false)
mergePropertiesIntoSelection Within Krita's SVG/CSS text system, it is possible to apply incomplete p...
void stopBlinkCursor()
Called by timer, stops the text blinking animation.
QList< QAction * > actions
int getAnchor()
Get the current selection anchor. This is the same as position, unless there's a selection.
void insertText(QString text)
Insert text at getPos()
void removeTransformsFromRange()
removeTransformsFromRange Called by actions to remove svg character transforms from range.
bool drawCursorInAdditionToSelection
QPair< KoSvgTextProperties, KoSvgTextProperties > currentTextProperties() const
currentTextProperties
void paintDecorations(QPainter &gc, QColor selectionColor, int decorationThickness=1, qreal handleRadius=5.0)
Paint all decorations and blinkingcursors.
QVector< IMEDecorationInfo > styleMap
Decoration info (underlines) for the preEdit string to differentiate it from regular text.
const QScopedPointer< Private > d
void deselectText()
Deselect all text. This effectively makes anchor the same as pos.
bool hasSelection() override
return true if the tool currently has something selected that can be copied or deleted.
void setCaretSetting(int cursorWidth=1, int cursorFlash=1000, int cursorFlashLimit=5000, bool drawCursorInAdditionToSelection=false)
setCaretSetting Set the caret settings for the cursor. Qt has some standard functionality associated,...
void setPos(int pos, int anchor)
Set the pos and the anchor.
void addCommandToUndoAdapter(KUndo2Command *cmd)
Adds a command to the canvas of the parent tool.
void clearFormattingAction()
Called by the clear formatting action.
void notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) override
QRectF oldIMEDecorationRect
Update Rectangle of previous decoration.
QString handleName(TypeSettingModeHandle handle) const
handleName
void sigOpenGlyphPalette()
Called by actions, tells the parent tool to open the glyph palette.
void propertyAction()
Called by the actions to execute a property change based on their data.
void blinkCursor()
Called by timer, toggles the text cursor visible or invisible.
void updateCanvasResources()
Update the canvas resources with fore and background color.
SvgTextCursor(KoCanvasBase *canvas)
TypeSettingModeHandle
Handles used by type setting mode.
@ BaselineAlphabetic
Baselines.
@ BaselineShift
The text properties handles.
void inputMethodEvent(QInputMethodEvent *event)
Process an input method event. This is used by IME like virtual keyboards.
void canvasResourceChanged(int key, const QVariant &value)
Called when the canvas resources (foreground/background) change.
void setPosToPoint(QPointF point, bool moveAnchor=true)
Set the pos from a point. This currently does a search inside the text shape.
QCursor cursorTypeForTypeSetting() const
Return appropriate typeSetting cursor;.
void moveCursor(MoveMode mode, bool moveAnchor=true)
Move the cursor, and, if you don't want a selection, move the anchor.
void setPasteRichTextByDefault(const bool pasteRichText=true)
setPasteRichText
bool acceptableInput(const QKeyEvent *event) const
More or less copied from bool QInputControl::isAcceptableInput(const QKeyEvent *event) const.
QList< KoSvgTextProperties > propertiesForRange() const
propertiesForRange
TypeSettingDecorInfo typeSettingDecor
QVariant inputMethodQuery(Qt::InputMethodQuery query) const
Process an input method query and return the requested result.
void removeSelection()
removeSelection if there's a selection, creates a text-removal command.
QPainterPath selection
void setTypeSettingModeActive(bool activate)
Set type setting mode active.
void updateTypeSettingDecoration()
void removeText(MoveMode first, MoveMode second)
removeText remove text relative to the current position. This will move the cursor according to the m...
bool setDominantBaselineFromHandle(const TypeSettingModeHandle handle)
setDominantBaselineFromHandle Set the dominant baseline from a given handle.
int moveModeResult(const MoveMode mode, int &pos, bool visual=false) const
Processes a move action, returns the input.
KoCanvasBase * canvas
int getPos()
Get the current position.
void focusOut()
Stops blinking cursor.
void removeLastCodePoint()
removeLastCodePoint Special function to remove the last code point. Triggered by backspace....
int posForTypeSettingHandleAndRect(const TypeSettingModeHandle handle, const QRectF regionOfInterest)
posForHandleAndRect Returns the closest cursor position for a given region and typesetting handle....
QPainterPath cursorShape
QRectF oldTypeSettingRect
TypeSettingModeHandle typeSettingHandleAtPos(const QRectF regionOfInterest)
Get typeSettingMode handle for text;.
void notifyMarkupChanged() override
bool pastePlainText()
pastePlainText Explicitely paste plaintext at pos.
bool registerPropertyAction(QAction *action, const QString &name)
Register an action.
void selectionChanged()
Sents an update selection was changed.
void focusIn()
Turns on blinking cursor.
void updateCursorDecoration(QRectF updateRect)
Sents an update to the parent tool to update it's decorations.
void setDrawTypeSettingHandle(bool draw)
bool paste()
paste pastes plain text in the clipboard at pos. Uses pasteRichTextByDefault to determine whether to ...
QPainterPath IMEDecoration
The decorations for the current preedit string.
void updateTypeSettingDecorFromShape()
Update the type setting decorations.
void updateModifiers(const Qt::KeyboardModifiers modifiers)
KisAcyclicSignalConnector resourceManagerAcyclicConnector
QList< KoSvgTextProperties > propertiesForShape() const
propertiesForShape
void setShape(KoSvgTextShape *textShape)
setShape
void updateInputMethodItemTransform()
Qt::KeyboardModifiers lastKnownModifiers
void insertRichText(KoSvgTextShape *insert, bool inheritPropertiesIfPossible=false)
Insert rich text at getPos();.
SvgTextRemoveCommand * removeSelectionImpl(bool allowCleanUp, KUndo2Command *parent=0)
removeSelection if there's a selection, creates a text-removal command.
void notifyCursorPosChanged(int pos, int anchor) override
KoSvgTextShape * shape
void copy() const
copy copies plain text into the clipboard between anchor and pos.
void updateCursor(bool firstUpdate=false)
update the cursor shape. First update will block ensuring the canvas is visible so setShape won't cau...
QMap< SvgTextCursor::TypeSettingModeHandle, QPainterPath > paths
QMap< SvgTextCursor::TypeSettingModeHandle, QPainterPath > baselines
QMap< SvgTextCursor::TypeSettingModeHandle, QPainterPath > parentBaselines
QPair< QPointF, QPointF > handles
QMap< SvgTextCursor::TypeSettingModeHandle, QPainterPath > parentPaths
QRectF boundingRect(qreal handleRadius)
bool testBaselines(Qt::KeyboardModifiers modifiers)