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
48 int start = -1;
49 int length = 0;
50 KoSvgText::TextDecorations decor = KoSvgText::DecorationNone;
52 bool thick = false;
53
54 void setDecorationFromQStyle(QTextCharFormat::UnderlineStyle s) {
55 // whenever qt sets an underlinestyle it always sets the underline.
56 decor.setFlag(KoSvgText::DecorationUnderline, s != QTextCharFormat::NoUnderline);
57 if (s == QTextCharFormat::DotLine) {
59 } else if (s == QTextCharFormat::DashUnderline) {
61 } else if (s == QTextCharFormat::WaveUnderline) {
63 } else if (s == QTextCharFormat::SpellCheckUnderline) {
65#ifdef Q_OS_MACOS
67#endif
68 } else {
70 }
71 }
72
73 void setDecorationFromQTextCharFormat(QTextCharFormat format) {
74 if (format.hasProperty(QTextFormat::FontUnderline)) {
75 decor.setFlag(KoSvgText::DecorationUnderline, format.property(QTextFormat::FontUnderline).toBool());
76 }
77 if (format.hasProperty(QTextFormat::FontOverline)) {
78 decor.setFlag(KoSvgText::DecorationOverline, format.property(QTextFormat::FontOverline).toBool());
79 }
80 if (format.hasProperty(QTextFormat::FontStrikeOut)) {
81 decor.setFlag(KoSvgText::DecorationLineThrough, format.property(QTextFormat::FontStrikeOut).toBool());
82 }
83
84 if (format.hasProperty(QTextFormat::TextUnderlineStyle)) {
85 setDecorationFromQStyle(format.underlineStyle());
86 }
93 if (format.hasProperty(QTextFormat::BackgroundBrush)) {
94 thick = format.background().isOpaque();
95#ifdef Q_OS_LINUX
96 if (style == KoSvgText::Dashed) {
98 }
99#endif
100
101 }
103 // Ensure a underline is always set.
105 }
106 }
107};
108
110 QPair<QPointF, QPointF> handles;
112
113 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> baselines;
114 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths;
115
116 QPainterPath edges;
117 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentPaths;
118 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentBaselines;
119
121
122 QRectF boundingRect(qreal handleRadius) {
123 QRectF total;
124 for (int i = 0; i< paths.values().size(); i++) {
125 total |= paths.values().at(i).boundingRect();
126 }
127 for (int i = 0; i< baselines.values().size(); i++) {
128 total |= baselines.values().at(i).boundingRect();
129 }
130 for (int i = 0; i< parentPaths.values().size(); i++) {
131 total |= parentPaths.values().at(i).boundingRect();
132 }
133 for (int i = 0; i< parentBaselines.values().size(); i++) {
134 total |= parentBaselines.values().at(i).boundingRect();
135 }
136 total |= edges.boundingRect();
137 QRectF rect(0, 0, handleRadius, handleRadius);
138 rect.moveCenter(handles.first);
139 total |= rect;
140 rect.moveCenter(handles.second);
141 total |= rect;
142 return total;
143 }
144
145 bool testBaselines(Qt::KeyboardModifiers modifiers) {
146 return (modifiers & Qt::ShiftModifier);
147 }
148
149};
150
151struct Q_DECL_HIDDEN SvgTextCursor::Private {
153 {
154 public:
156 : m_d(d)
157 // Only unblock updates if they were blocked to begin with.
158 , m_unblockQueryUpdates(!std::exchange(d->blockQueryUpdates, true))
159 {
160 }
161
162 InputQueryUpdateBlocker(const QScopedPointer<Private> &d)
164 {
165 }
166
168 {
169 if (m_d) {
170 QInputMethod *inputMethod = QGuiApplication::inputMethod();
171
172 if (m_unblockQueryUpdates) {
173 m_d->blockQueryUpdates = false;
174 inputMethod->update(Qt::ImQueryInput);
175 }
176
177 if (m_changeVisibility) {
178 inputMethod->setVisible(m_d->shape != nullptr);
179 }
180 }
181 }
182
183 void setChangeVisibility(bool changeVisibility)
184 {
185 m_changeVisibility = changeVisibility;
186 }
187
188 private:
191 bool m_changeVisibility = false;
192 };
193
195 bool isAddingCommand = false;
196 int pos = 0;
197 int anchor = 0;
198 KoSvgTextShape *shape {nullptr};
199
202 bool cursorVisible = false;
203 bool hasFocus = false;
204
205 QPainterPath cursorShape;
210 int cursorWidth = 1;
211 bool drawCursorInAdditionToSelection = false;
212 QPainterPath selection;
214
215
216 // This is used to adjust cursorpositions better on text-shape relayouts.
217 int posIndex = 0;
218 int anchorIndex = 0;
219
220 bool visualNavigation = true;
221 bool pasteRichText = true;
222
223 Qt::KeyboardModifiers lastKnownModifiers;
224
225 bool typeSettingMode = false;
227 bool drawTypeSettingHandle = true;
228 qreal handleRadius = 7;
231
232 SvgTextInsertCommand *preEditCommand {nullptr};
233 int preEditStart = -1;
234 int preEditLength = -1;
236 QPainterPath IMEDecoration;
238 bool blockQueryUpdates = false;
239
241
243
245};
246
248 d(new Private)
249{
250 d->canvas = canvas;
251 d->interface = new SvgTextCursorPropertyInterface(this);
252 if (d->canvas->canvasController()) {
253 // Mockcanvas in the tests has no canvas controller.
254 connect(d->canvas->canvasController()->proxyObject, SIGNAL(sizeChanged(QSize)), this, SLOT(updateInputMethodItemTransform()));
255 connect(d->canvas->canvasController()->proxyObject,
256 SIGNAL(moveDocumentOffset(QPointF, QPointF)),
257 this,
259 connect(d->canvas->canvasController()->proxyObject,
260 SIGNAL(effectiveZoomChanged(qreal)),
261 this,
263 connect(d->canvas->canvasController()->proxyObject,
264 SIGNAL(documentRotationChanged(qreal)),
265 this,
267 connect(d->canvas->canvasController()->proxyObject,
268 SIGNAL(documentMirrorStatusChanged(bool, bool)),
269 this,
271 d->resourceManagerAcyclicConnector.connectBackwardResourcePair(
272 d->canvas->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)),
273 this, SLOT(canvasResourceChanged(int,QVariant)));
274 d->resourceManagerAcyclicConnector.connectForwardVoid(d->interface, SIGNAL(textCharacterSelectionChanged()), this, SLOT(updateCanvasResources()));
275 }
276
277}
278
280{
282 d->cursorFlash.stop();
283 d->cursorFlashLimit.stop();
284 d->shape = nullptr;
285}
286
288{
289 return d->shape;
290}
291
293{
294 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
295 inputQueryUpdateBlocker.setChangeVisibility(true);
296
297 if (d->shape) {
299 d->shape->removeShapeChangeListener(this);
300 }
301 d->shape = textShape;
302 if (d->shape) {
303 d->shape->addShapeChangeListener(this);
305 d->pos = d->shape->posForIndex(d->shape->plainText().size());
307 } else {
308 d->pos = 0;
309 }
310 d->anchor = 0;
311 updateCursor(true);
313 d->interface->emitSelectionChange();
314}
315
316void SvgTextCursor::setCaretSetting(int cursorWidth, int cursorFlash, int cursorFlashLimit, bool drawCursorInAdditionToSelection)
317{
318 d->cursorFlash.setInterval(cursorFlash/2);
319 d->cursorFlashLimit.setInterval(cursorFlashLimit);
320 d->cursorWidth = cursorWidth;
321 d->drawCursorInAdditionToSelection = drawCursorInAdditionToSelection;
322 connect(&d->cursorFlash, SIGNAL(timeout()), this, SLOT(blinkCursor()));
323 connect(&d->cursorFlashLimit, SIGNAL(timeout()), this, SLOT(stopBlinkCursor()));
324}
325
326void SvgTextCursor::setVisualMode(bool visualMode)
327{
328 d->visualNavigation = visualMode;
329}
330
331void SvgTextCursor::setPasteRichTextByDefault(const bool pasteRichText)
332{
333 d->pasteRichText = pasteRichText;
334}
335
337{
338 d->typeSettingMode = activate;
340}
341
343{
344 return d->pos;
345}
346
348{
349 return d->anchor;
350}
351
352void SvgTextCursor::setPos(int pos, int anchor)
353{
354 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
355 d->pos = pos;
356 d->anchor = anchor;
357 updateCursor();
359}
360
361void SvgTextCursor::setPosToPoint(QPointF point, bool moveAnchor)
362{
363 if (d->shape) {
364 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
365 int pos = d->shape->posForPointLineSensitive(d->shape->documentToShape(point));
366 if (d->preEditCommand) {
367 int start = d->shape->indexForPos(d->preEditStart);
368 int end = start + d->preEditLength;
369 int posIndex = d->shape->indexForPos(pos);
370 if (posIndex > start && posIndex <= end) {
371 qApp->inputMethod()->invokeAction(QInputMethod::Click, posIndex - start);
372 return;
373 } else {
375 }
376 }
377
378 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
379 d->pos = qBound(0, pos, finalPos);
380 if (moveAnchor || d->anchor < 0 || d->anchor > finalPos) {
381 d->anchor = d->pos;
382 }
383 updateCursor();
385 }
386}
387
389{
391
392 if (!(d->typeSettingMode && d->shape && d->canvas)) return handle;
393
394 const QRectF roiInShape = d->shape->absoluteTransformation().inverted().mapRect(regionOfInterest);
395
396 if (d->typeSettingDecor.handlesEnabled) {
397 if (roiInShape.contains(d->typeSettingDecor.handles.first)) {
399 } else if (roiInShape.contains(d->typeSettingDecor.handles.second)) {
400 handle = SvgTextCursor::EndPos;
401 }
402 }
403 if (handle != NoHandle) return handle;
404 if (!d->typeSettingDecor.boundingRect(d->handleRadius).intersects(roiInShape)) return handle;
405
406 qreal closest = std::numeric_limits<qreal>::max();
407 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths
408 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.baselines: d->typeSettingDecor.paths;
409 Q_FOREACH (const SvgTextCursor::TypeSettingModeHandle baseline, paths.keys()) {
410 const QPainterPath path = paths.value(baseline);
411 if (!path.intersects(roiInShape)) continue;
412
413 const QList<QPolygonF> polys = path.toSubpathPolygons();
414 Q_FOREACH(const QPolygonF poly, polys) {
415 if (poly.size() < 2) continue;
416 for (int i = 1; i < poly.size(); i++) {
417 QLineF l(poly.at(i-1), poly.at(i));
418 qreal distance = kisDistanceToLine(roiInShape.center(), l);
419 if (distance < closest) {
420 handle = baseline;
421 closest = distance;
422 d->typeSettingDecor.closestBaselinePoint =
423 kisProjectOnVector(l.p2() - l.p1(), roiInShape.center() - l.p1()) + l.p1();
424 }
425 }
426 }
427 }
428
429 return handle;
430}
431
433{
434 d->hoveredTypeSettingHandle = hovered;
435}
436
438{
439 d->drawTypeSettingHandle = draw;
440}
441
443{
444 if (d->shape) {
445 d->typeSettingDecor.handlesEnabled = ((d->shape->textType() == KoSvgTextShape::PreformattedText
446 || d->shape->textType() == KoSvgTextShape::PrePositionedText)
447 && !(d->shape->topLevelNodeForPos(d->pos).textPath()));
448 }
449}
450
452{
453 if (d->hoveredTypeSettingHandle == StartPos ||
454 d->hoveredTypeSettingHandle == StartPos ||
455 d->typeSettingDecor.testBaselines(d->lastKnownModifiers)) {
456 return Qt::ArrowCursor;
457 } else if (d->shape) {
458 return (d->shape->writingMode() == KoSvgText::HorizontalTB)? Qt::SizeVerCursor: Qt::SizeHorCursor;
459 }
460 return Qt::ArrowCursor;
461}
462
464{
465 bool baseline = d->typeSettingDecor.testBaselines(d->lastKnownModifiers);
466 if (handle == Ascender) {
467 if (baseline) {
468 return i18nc("Type setting mode line name", "Text Top");
469 } else {
470 return i18nc("Type setting mode line name", "Font Size");
471 }
472 } else if (handle == Descender) {
473 if (baseline) {
474 return i18nc("Type setting mode line name", "Text Bottom");
475 } else {
476 return i18nc("Type setting mode line name", "Font Size");
477 }
478 } else if (handle == BaselineAlphabetic) {
479 return i18nc("Type setting mode line name", "Alphabetic");
480 } else if (handle == BaselineIdeographic) {
481 return i18nc("Type setting mode line name", "Ideographic");
482 } else if (handle == BaselineHanging) {
483 return i18nc("Type setting mode line name", "Hanging");
484 } else if (handle == BaselineMiddle) {
485 return i18nc("Type setting mode line name", "Middle");
486 } else if (handle == BaselineMathematical) {
487 return i18nc("Type setting mode line name", "Mathematical");
488 } else if (handle == BaselineCentral) {
489 return i18nc("Type setting mode line name", "Central");
490 } else if (handle == LineHeightTop || handle == LineHeightBottom) {
491 return i18nc("Type setting mode line name", "Line Height");
492 } else if (handle == BaselineShift) {
493 if (baseline) {
494 return i18nc("Type setting mode line name", "Current Baseline");
495 } else {
496 return i18nc("Type setting mode line name", "Baseline Shift");
497 }
498 } else {
499 return QString();
500 }
501}
502
504{
505 if (handle == NoHandle) return false;
506 if (!d->typeSettingDecor.testBaselines(d->lastKnownModifiers)) return false;
508 if (handle == Ascender) {
510 } else if (handle == Descender) {
512 } else if (handle == BaselineAlphabetic) {
514 } else if (handle == BaselineIdeographic) {
516 } else if (handle == BaselineHanging) {
518 } else if (handle == BaselineMiddle) {
519 baseline = KoSvgText::BaselineMiddle;
520 } else if (handle == BaselineMathematical) {
522 } else if (handle == BaselineCentral) {
524 } else {
525 return false;
526 }
528 props.setProperty(KoSvgTextProperties::DominantBaselineId, QVariant::fromValue(baseline));
529 props.setProperty(KoSvgTextProperties::AlignmentBaselineId, QVariant::fromValue(baseline));
531 return true;
532}
533
534QMap<SvgTextCursor::TypeSettingModeHandle, int> typeSettingBaselinesFromMetrics(const KoSvgText::FontMetrics metrics, const qreal lineGap, const bool isHorizontal) {
535 return QMap<SvgTextCursor::TypeSettingModeHandle, int> {
543 {SvgTextCursor::BaselineMiddle, isHorizontal? metrics.xHeight/2: metrics.ideographicCenterBaseline},
544 {SvgTextCursor::LineHeightTop, metrics.ascender+(lineGap/2)},
545 {SvgTextCursor::LineHeightBottom, metrics.descender-(lineGap/2)},
547 };
548}
549
550int SvgTextCursor::posForTypeSettingHandleAndRect(const TypeSettingModeHandle handle, const QRectF regionOfInterest)
551{
552 if (!d->shape) return 0;
553
555 d->shape->getPositionsAndRotationsForRange(d->pos, d->anchor);
556 if (infos.size() < 1) return 0;
557
558 const QRectF roi = d->shape->documentToShape(regionOfInterest);
559
560 for (auto it = infos.begin(); it != infos.end(); it++) {
561 const int currentPos = (d->pos == d->anchor)? -1 :d->shape->posForIndex(it->logicalIndex);
562 const KoSvgTextProperties props = d->shape->propertiesForPos(currentPos, true);
564
565 KoSvgText::FontMetrics metrics = (d->pos == d->anchor)? props.metrics(true, true): it->metrics;
566 const bool isHorizontal = d->shape->writingMode() == KoSvgText::HorizontalTB;
567
568 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
569 const int lineGap = lineHeight.isNormal? metrics.lineGap: (lineHeight.length.value/scaleMetrics)-(metrics.ascender-metrics.descender);
570
571 QTransform t = QTransform::fromTranslate(it->finalPos.x(), it->finalPos.y());
572 t.rotate(it->rotateDeg);
573
574 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
575 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
576
577 const int metric = types.value(handle);
578 QPointF offset = isHorizontal? QPointF(0, -(metric*scaleMetrics)): QPointF(metric*scaleMetrics, 0);
579 QLineF line = t.map(QLineF(offset, offset+it->advance));
580 if (KisAlgebra2D::intersectLineRect(line, roi.toAlignedRect(), false)) return d->shape->posForIndex(it->logicalIndex);
581 }
582
583 return 0;
584}
585
586
587
588void SvgTextCursor::moveCursor(MoveMode mode, bool moveAnchor)
589{
590 if (d->shape) {
591
592 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
593 d->pos = qBound(0, moveModeResult(mode, d->pos, d->visualNavigation), finalPos);
594
595 if (moveAnchor) {
596 d->anchor = d->pos;
597 }
599 updateCursor();
600 }
601}
602
604{
605
606 if (d->shape) {
607 //KUndo2Command *parentCmd = new KUndo2Command;
608 if (hasSelection()) {
609 SvgTextRemoveCommand *removeCmd = removeSelectionImpl(false);
610 addCommandToUndoAdapter(removeCmd);
611 }
612
613 SvgTextInsertCommand *insertCmd = new SvgTextInsertCommand(d->shape, d->pos, d->anchor, text);
614 addCommandToUndoAdapter(insertCmd);
615
616 }
617}
618
619void SvgTextCursor::insertRichText(KoSvgTextShape *insert, bool inheritPropertiesIfPossible)
620{
621 if (d->shape) {
622 //KUndo2Command *parentCmd = new KUndo2Command;
623 if (hasSelection()) {
624 SvgTextRemoveCommand *removeCmd = removeSelectionImpl(false);
625 addCommandToUndoAdapter(removeCmd);
626 }
627
628 SvgTextInsertRichCommand *cmd = new SvgTextInsertRichCommand(d->shape, insert, d->pos, d->anchor, inheritPropertiesIfPossible);
630
631 }
632}
633
635{
636 if (d->shape) {
637 SvgTextRemoveCommand *removeCmd;
638 if (hasSelection()) {
639 removeCmd = removeSelectionImpl(true);
640 addCommandToUndoAdapter(removeCmd);
641 } else {
642 int posA = moveModeResult(first, d->pos, d->visualNavigation);
643 int posB = moveModeResult(second, d->pos, d->visualNavigation);
644
645 int posStart = qMin(posA, posB);
646 int posEnd = qMax(posA, posB);
647 int indexEnd = d->shape->indexForPos(posEnd);
648 int length = indexEnd - d->shape->indexForPos(posStart);
649
650 removeCmd = new SvgTextRemoveCommand(d->shape, indexEnd, d->pos, d->anchor, length, true);
651 addCommandToUndoAdapter(removeCmd);
652 }
653 }
654}
655
657{
658 if (d->shape) {
659 SvgTextRemoveCommand *removeCmd;
660 if (hasSelection()) {
661 removeCmd = removeSelectionImpl(true);
662 addCommandToUndoAdapter(removeCmd);
663 } else {
664 int lastIndex = d->shape->indexForPos(d->pos);
665 removeCmd = new SvgTextRemoveCommand(d->shape, lastIndex, d->pos, d->anchor, 1, true);
666 addCommandToUndoAdapter(removeCmd);
667 }
668 }
669}
670
671QPair<KoSvgTextProperties, KoSvgTextProperties> SvgTextCursor::currentTextProperties() const
672{
673 if (d->shape) {
674 return QPair<KoSvgTextProperties, KoSvgTextProperties>(d->shape->propertiesForPos(d->pos), d->shape->propertiesForPos(d->pos, true));
675 }
676 return QPair<KoSvgTextProperties, KoSvgTextProperties>();
677}
678
680{
681 if (!d->shape) return QList<KoSvgTextProperties>();
682 int start = -1;
683 int end = -1;
684 start = qMin(d->pos, d->anchor);
685 end = qMax(d->pos, d->anchor);
686 return d->shape->propertiesForRange(start, end);
687}
688
690{
691 if (!d->shape) return QList<KoSvgTextProperties>();
692 return {d->shape->propertiesForRange(-1, -1)};
693}
694
695void SvgTextCursor::mergePropertiesIntoSelection(const KoSvgTextProperties props, const QSet<KoSvgTextProperties::PropertyId> removeProperties, bool paragraphOnly, bool selectWord)
696{
697 if (d->shape) {
698 int start = -1;
699 int end = -1;
700 if (!paragraphOnly) {
701 start = d->pos;
702 end = d->anchor;
703 }
704 if (selectWord && d->pos == d->anchor) {
705 const int finalPos = d->shape->posForIndex(d->shape->plainText().size());
706 start = qBound(0, moveModeResult(MoveWordStart, d->pos, d->visualNavigation), finalPos);
707 end = qBound(0, moveModeResult(MoveWordEnd, d->pos, d->visualNavigation), finalPos);
708 }
709 KUndo2Command *cmd = new SvgTextMergePropertiesRangeCommand(d->shape, props, start, end, removeProperties);
711 }
712}
713
715{
716 KUndo2Command *removeCmd = removeSelectionImpl(true);
717 addCommandToUndoAdapter(removeCmd);
718}
719
721{
722 SvgTextRemoveCommand *removeCmd = nullptr;
723 if (d->shape) {
724 if (d->anchor != d->pos) {
725 int end = d->shape->indexForPos(qMax(d->anchor, d->pos));
726 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - d->shape->indexForPos(qMin(d->anchor, d->pos));
727 removeCmd = new SvgTextRemoveCommand(d->shape, end, d->pos, d->anchor, length, allowCleanUp, parent);
728 }
729 }
730 return removeCmd;
731}
732
734{
735 if (d->shape) {
736 int start = d->shape->indexForPos(qMin(d->anchor, d->pos));
737 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - start;
738 QString copied = d->shape->plainText().mid(start, length);
739 std::unique_ptr<KoSvgTextShape> copy = d->shape->copyRange(start, length);
740 QClipboard *cb = QApplication::clipboard();
741
742 if (copy) {
743 KoSvgTextShapeMarkupConverter converter(copy.get());
744 QString svg;
745 QString styles;
746 QString html;
747 QMimeData *svgData = new QMimeData();
748 if (converter.convertToSvg(&svg, &styles)) {
749 QString svgDoc = QString("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"2.0\">%1\n%2</svg>").arg(styles).arg(svg);
750 svgData->setData(QLatin1String("image/svg+xml"), svgDoc.toUtf8());
751 }
752 svgData->setText(copied);
753 if (converter.convertToHtml(&html))
754 svgData->setHtml(html);
755 cb->setMimeData(svgData);
756 } else {
757 cb->setText(copied);
758 }
759
760 }
761}
762
764{
765 bool success = d->pasteRichText? pasteRichText(): pastePlainText();
766 return success;
767}
768
770{
771 bool success = false;
772 if (d->shape) {
773 QClipboard *cb = QApplication::clipboard();
774 const QMimeData *mimeData = cb->mimeData();
775 KoSvgPaste shapePaste;
776 if (shapePaste.hasShapes()) {
777 QList<KoShape*> shapes = shapePaste.fetchShapes(d->shape->boundingRect(), 72.0);
778 while (shapes.size() > 0) {
779 KoSvgTextShape *textShape = dynamic_cast<KoSvgTextShape*>(shapes.takeFirst());
780 if (textShape) {
781 insertRichText(textShape);
782 success = true;
783 }
784 }
785 } else if (mimeData->hasHtml()) {
786 QString html = mimeData->html();
787 KoSvgTextShape *insert = new KoSvgTextShape();
788 KoSvgTextShapeMarkupConverter converter(insert);
789 QString svg;
790 QString styles;
791 if (converter.convertFromHtml(html, &svg, &styles)
792 && converter.convertFromSvg(svg, styles, d->shape->boundingRect(), 72.0) ) {
793 insertRichText(insert);
794 success = true;
795 }
796 }
797
798 if (!success) {
799 success = pastePlainText();
800 }
801 }
802 return success;
803}
804
806{
807 bool success = false;
808 QClipboard *cb = QApplication::clipboard();
809 const QMimeData *mimeData = cb->mimeData();
810 if (mimeData->hasText()) {
811 insertText(mimeData->text());
812 success = true;
813 }
814 return success;
815}
816
818{
819 if (d->shape) {
820 KUndo2Command *cmd = new SvgTextRemoveTransformsFromRange(d->shape, d->pos, d->anchor);
822 }
823}
824
826{
827 setPos(d->pos, d->pos);
828}
829
830static QColor bgColorForCaret(QColor c, int opacity = 64) {
831
832 return KisPaintingTweaks::luminosityCoarse(c) > 0.8? QColor(0, 0, 0, opacity) : QColor(255, 255, 255, opacity);
833}
834
835void SvgTextCursor::paintDecorations(QPainter &gc, QColor selectionColor, int decorationThickness, qreal handleRadius)
836{
837 if (d->shape) {
838 gc.save();
839 gc.setTransform(d->shape->absoluteTransformation(), true);
840
841 if (d->pos != d->anchor && !d->typeSettingMode) {
842 gc.save();
843 gc.setOpacity(0.5);
844 QBrush brush(selectionColor);
845 gc.fillPath(d->selection, brush);
846 gc.restore();
847 }
848
849 if ( (d->drawCursorInAdditionToSelection || d->pos == d->anchor)
850 && d->cursorVisible) {
851 QPen pen;
852 pen.setCosmetic(true);
853 QColor c = d->cursorColor.isValid()? d->cursorColor: Qt::black;
854 pen.setColor(bgColorForCaret(c));
855 pen.setWidth((d->cursorWidth + 2) * decorationThickness);
856 gc.setPen(pen);
857 gc.drawPath(d->cursorShape);
858 pen.setColor(c);
859 pen.setWidth(d->cursorWidth * decorationThickness);
860 gc.setPen(pen);
861 gc.drawPath(d->cursorShape);
862
863 }
864
865 if (d->preEditCommand) {
866 gc.save();
867 QBrush brush(selectionColor);
868 gc.setOpacity(0.5);
869 gc.fillPath(d->IMEDecoration, brush);
870 gc.restore();
871 }
872 if (d->typeSettingMode && d->drawTypeSettingHandle) {
873 d->handleRadius = handleRadius;
874 QTransform painterTf = gc.transform();
875 KisHandlePainterHelper helper(&gc, handleRadius, decorationThickness);
878
879 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> paths
880 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.baselines: d->typeSettingDecor.paths;
881 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> parentPaths
882 = d->typeSettingDecor.testBaselines(d->lastKnownModifiers)? d->typeSettingDecor.parentBaselines: d->typeSettingDecor.parentPaths;
883 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, paths.keys()) {
884 const QPainterPath p = paths.value(handle);
885 const QPainterPath parent = parentPaths.value(handle);
886 if (d->hoveredTypeSettingHandle == handle) {
887 helper.setHandleStyle(highlight);
888 helper.drawPath(parent);
889 helper.drawPath(p);
890 } else {
891 gc.save();
892 QPen pen(selectionColor, decorationThickness, handle == BaselineShift? Qt::SolidLine: Qt::DashLine);
893 pen.setCosmetic(true);
894 gc.setPen(pen);
895 gc.setOpacity(0.5);
896 gc.drawPath(painterTf.map(parent));
897 gc.drawPath(painterTf.map(p));
898 gc.restore();
899 }
900 gc.save();
901 QPen pen(selectionColor, decorationThickness, Qt::SolidLine);
902 pen.setCosmetic(true);
903 gc.setPen(pen);
904 gc.setOpacity(0.5);
905 gc.drawPath(painterTf.map(d->typeSettingDecor.edges));
906 gc.restore();
907 }
908
909 if (d->typeSettingDecor.handlesEnabled) {
910 helper.setHandleStyle(d->hoveredTypeSettingHandle == EndPos? highlight: regular);
911 helper.drawHandleCircle(d->typeSettingDecor.handles.second);
912 helper.setHandleStyle(d->hoveredTypeSettingHandle == StartPos? highlight: regular);
913 helper.drawHandleRect(d->typeSettingDecor.handles.first);
914 }
915 QString name = handleName(d->hoveredTypeSettingHandle);
916 if (!name.isEmpty()) {
917 QPainterPath textP;
918 // When we're drawing on opengl, there's no anti-aliasing, so we should have full hinting for readabiltiy.
919 QFont font = gc.font();
920 font.setHintingPreference(QFont::PreferFullHinting);
921 textP.addText(painterTf.map(d->typeSettingDecor.closestBaselinePoint).toPoint(), font, name);
922 gc.save();
923 QPen pen(bgColorForCaret(selectionColor, 255));
924 pen.setCosmetic(true);
925 pen.setWidth(decorationThickness);
926 gc.setPen(pen);
927 gc.drawPath(textP);
928 gc.fillPath(textP, QBrush(selectionColor));
929 gc.restore();
930 }
931
932 }
933 gc.restore();
934 }
935}
936
937QVariant SvgTextCursor::inputMethodQuery(Qt::InputMethodQuery query) const
938{
939 dbgTools << "receiving inputmethod query" << query;
940
941 // Because we set the input item transform to be shape->document->view->widget->window,
942 // the coordinates here should be in shape coordinates.
943 switch(query) {
944 case Qt::ImEnabled:
945 return d->shape? true: false;
946 break;
947 case Qt::ImCursorRectangle:
948 // The platform integration will always define the cursor as the 'left side' handle.
949 if (d->shape) {
950 QPointF caret1(d->cursorCaret.p1());
951 QPointF caret2(d->cursorCaret.p2());
952
953
954 QRectF rect = QRectF(caret1, caret2).normalized();
955 if (!rect.isValid()) {
956 if (rect.height() < 1) {
957 rect.adjust(0, -1, 0, 0);
958 }
959 if (rect.width() < 1) {
960 rect.adjust(0, 0, 1, 0);
961 }
962
963 }
964 return rect.toAlignedRect();
965 }
966 break;
967 case Qt::ImAnchorRectangle:
968 // The platform integration will always define the anchor as the 'right side' handle.
969 if (d->shape) {
970 QPointF caret1(d->anchorCaret.p1());
971 QPointF caret2(d->anchorCaret.p2());
972 QRectF rect = QRectF(caret1, caret2).normalized();
973 if (rect.isEmpty()) {
974 if (rect.height() < 1) {
975 rect.adjust(0, -1, 0, 0);
976 }
977 if (rect.width() < 1) {
978 rect = rect.adjusted(-1, 0, 0, 0).normalized();
979 }
980 }
981 return rect.toAlignedRect();
982 }
983 break;
984 //case Qt::ImFont: // not sure what this is used for, but we cannot sent out without access to properties.
985 case Qt::ImAbsolutePosition:
986 case Qt::ImCursorPosition:
987 if (d->shape) {
988 return d->shape->indexForPos(d->pos);
989 }
990 break;
991 case Qt::ImSurroundingText:
992 if (d->shape) {
993 QString surroundingText = d->shape->plainText();
994 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
995 surroundingText.remove(preEditIndex, d->preEditLength);
996 return surroundingText;
997 }
998 break;
999 case Qt::ImCurrentSelection:
1000 if (d->shape) {
1001 QString surroundingText = d->shape->plainText();
1002 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1003 surroundingText.remove(preEditIndex, d->preEditLength);
1004 int start = d->shape->indexForPos(qMin(d->anchor, d->pos));
1005 int length = d->shape->indexForPos(qMax(d->anchor, d->pos)) - start;
1006 return surroundingText.mid(start, length);
1007 }
1008 break;
1009 case Qt::ImTextBeforeCursor:
1010 if (d->shape) {
1011 int start = d->shape->indexForPos(d->pos);
1012 QString surroundingText = d->shape->plainText();
1013 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1014 surroundingText.remove(preEditIndex, d->preEditLength);
1015 return surroundingText.left(start);
1016 }
1017 break;
1018 case Qt::ImTextAfterCursor:
1019 if (d->shape) {
1020 int start = d->shape->indexForPos(d->pos);
1021 QString surroundingText = d->shape->plainText();
1022 int preEditIndex = d->preEditCommand? d->shape->indexForPos(d->preEditStart): 0;
1023 surroundingText.remove(preEditIndex, d->preEditLength);
1024 return surroundingText.right(start);
1025 }
1026 break;
1027 case Qt::ImMaximumTextLength:
1028 return QVariant(); // infinite text length!
1029 break;
1030 case Qt::ImAnchorPosition:
1031 if (d->shape) {
1032 return d->shape->indexForPos(d->anchor);
1033 }
1034 break;
1035 case Qt::ImHints:
1036 // It would be great to use Qt::ImhNoTextHandles or Qt::ImhNoEditMenu,
1037 // but neither are implemented for anything but web platform integration
1038 return Qt::ImhMultiLine;
1039 break;
1040 // case Qt::ImPreferredLanguage: // requires access to properties.
1041 // case Qt::ImPlatformData: // this is only for iOS at time of writing.
1042 case Qt::ImEnterKeyType:
1043 if (d->shape) {
1044 return Qt::EnterKeyDefault; // because input method hint is always multiline, this will show a return key.
1045 }
1046 break;
1047 // case Qt::ImInputItemClipRectangle // whether the input item is clipped?
1048 default:
1049 return QVariant();
1050 }
1051 return QVariant();
1052}
1053
1054void SvgTextCursor::inputMethodEvent(QInputMethodEvent *event)
1055{
1056 dbgTools << "Commit:"<< event->commitString() << "predit:"<< event->preeditString();
1057 dbgTools << "Replacement:"<< event->replacementStart() << event->replacementLength();
1058
1059 QRectF updateRect = d->shape? d->shape->boundingRect(): QRectF();
1060 SvgTextShapeManagerBlocker blocker(d->canvas->shapeManager());
1061
1062 bool isGettingInput = !event->commitString().isEmpty() || !event->preeditString().isEmpty()
1063 || event->replacementLength() > 0;
1064
1065 // Remove previous preedit string.
1066 if (d->preEditCommand) {
1067 d->preEditCommand->undo();
1068 d->preEditCommand = 0;
1069 d->preEditStart = -1;
1070 d->preEditLength = -1;
1071 updateRect |= d->shape? d->shape->boundingRect(): QRectF();
1072 }
1073
1074 if (!d->shape || !isGettingInput) {
1075 blocker.unlock();
1076 d->canvas->shapeManager()->update(updateRect);
1077 event->ignore();
1078 return;
1079 }
1080
1081 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1082
1083 // remove the selection if any.
1085
1086 // set the text insertion pos to replacement start and also remove replacement length, if any.
1087 int originalPos = d->pos;
1088 int index = d->shape->indexForPos(d->pos) + event->replacementStart();
1089 d->pos = d->shape->posForIndex(index);
1090 if (event->replacementLength() > 0) {
1092 index + event->replacementLength(),
1093 originalPos,
1094 d->anchor,
1095 event->replacementLength(),
1096 false);
1098 }
1099
1100 // add the commit string, if any.
1101 if (!event->commitString().isEmpty()) {
1102 insertText(event->commitString());
1103 }
1104
1105 // set the selection...
1106 Q_FOREACH(const QInputMethodEvent::Attribute attribute, event->attributes()) {
1107 if (attribute.type == QInputMethodEvent::Selection) {
1108 d->pos = d->shape->posForIndex(attribute.start);
1109 int index = d->shape->indexForPos(d->pos);
1110 d->anchor = d->shape->posForIndex(index + attribute.length);
1111 }
1112 }
1113
1114
1115 // insert a preedit string, if any.
1116 if (!event->preeditString().isEmpty()) {
1117 int index = d->shape->indexForPos(d->pos);
1118 d->preEditCommand = new SvgTextInsertCommand(d->shape, d->pos, d->anchor, event->preeditString());
1119 d->preEditCommand->redo();
1120 d->preEditLength = event->preeditString().size();
1121 d->preEditStart = d->shape->posForIndex(index, true);
1122 } else {
1123 d->preEditCommand = 0;
1124 }
1125
1126 // Apply the cursor offset for the preedit.
1128 Q_FOREACH(const QInputMethodEvent::Attribute attribute, event->attributes()) {
1129 dbgTools << "attribute: "<< attribute.type << "start: " << attribute.start
1130 << "length: " << attribute.length << "val: " << attribute.value;
1131 // Text Format is about setting the look of the preedit string, and there can be multiple per event
1132 // we primarily interpret the underline. When a background color is set, we increase the underline
1133 // thickness, as that's what is actually supposed to happen according to the comments in the
1134 // platform input contexts for both macOS and Windows.
1135
1136 if (attribute.type == QInputMethodEvent::TextFormat) {
1137 QVariant val = attribute.value;
1138 QTextCharFormat form = val.value<QTextFormat>().toCharFormat();
1139
1140 if (attribute.length == 0 || attribute.start < 0 || !attribute.value.isValid()) {
1141 continue;
1142 }
1143
1144 int positionA = -1;
1145 int positionB = -1;
1146 if (!styleMap.isEmpty()) {
1147 for (int i = 0; i < styleMap.size(); i++) {
1148 if (attribute.start >= styleMap.at(i).start
1149 && attribute.start < styleMap.at(i).start + styleMap.at(i).length) {
1150 positionA = i;
1151 }
1152 if (attribute.start + attribute.length > styleMap.at(i).start
1153 && attribute.start + attribute.length <= styleMap.at(i).start + styleMap.at(i).length) {
1154 positionB = i;
1155 }
1156 }
1157
1158 if (positionA > -1 && positionA == positionB) {
1159 IMEDecorationInfo decoration1 = styleMap.at(positionA);
1160 IMEDecorationInfo decoration2 = decoration1;
1161 IMEDecorationInfo decoration3 = decoration1;
1162 decoration3.start = (attribute.start+attribute.length);
1163 decoration3.length = (decoration1.start + decoration1.length) - decoration3.start;
1164 decoration1.length = attribute.start - decoration1.start;
1165 decoration2.start = attribute.start;
1166 decoration2.length = attribute.length;
1167 if (decoration1.length > 0) {
1168 styleMap[positionA] = decoration1;
1169 if (decoration2.length > 0) {
1170 positionA += 1;
1171 styleMap.insert(positionA, decoration2);
1172 }
1173 } else {
1174 styleMap[positionA] = decoration2;
1175 }
1176 if (decoration3.length > 0) {
1177 styleMap.insert(positionA + 1, decoration3);
1178 }
1179 } else if (positionA > -1 && positionB > -1
1180 && positionA != positionB) {
1181 IMEDecorationInfo decoration1 = styleMap.at(positionA);
1182 IMEDecorationInfo decoration2 = decoration1;
1183 IMEDecorationInfo decoration3 = styleMap.at(positionB);
1184 IMEDecorationInfo decoration4 = decoration3;
1185 decoration2.length = (decoration1.start + decoration1.length) - attribute.start;
1186 decoration1.length = attribute.start - decoration1.start;
1187 decoration2.start = attribute.start;
1188
1189 decoration4.start = (attribute.start+attribute.length);
1190 decoration3.length = (decoration3.start + decoration3.length) - decoration4.start;
1191 decoration3.length = decoration4.start - decoration3.start;
1192 if (decoration1.length > 0) {
1193 styleMap[positionA] = decoration1;
1194 if (decoration2.length > 0) {
1195 positionA += 1;
1196 styleMap.insert(positionA, decoration2);
1197 }
1198 } else {
1199 styleMap[positionA] = decoration2;
1200 }
1201
1202 if (decoration3.length > 0) {
1203 styleMap[positionB] = decoration3;
1204 if (decoration4.length > 0) {
1205 styleMap.insert(positionB + 1, decoration4);
1206 }
1207 } else {
1208 styleMap[positionB] = decoration4;
1209 }
1210 }
1211 }
1212
1213 if (positionA > -1 && !styleMap.isEmpty()) {
1214
1215 for(int i = positionA; i <= positionB; i++) {
1216 IMEDecorationInfo decoration = styleMap.at(i);
1217 decoration.setDecorationFromQTextCharFormat(form);
1218 styleMap[i] = decoration;
1219 }
1220
1221 } else {
1222 IMEDecorationInfo decoration;
1223 decoration.start = attribute.start;
1224 decoration.length = attribute.length;
1225 decoration.setDecorationFromQTextCharFormat(form);
1226 styleMap.append(decoration);
1227 }
1228
1229 // QInputMethodEvent::Language is about setting the locale on the given preedit string, which is not possible yet.
1230 // QInputMethodEvent::Ruby is supposedly ruby info for the preedit string, but none of the platform integrations
1231 // actually implement this at time of writing, and it may have been something from a previous live of Qt's.
1232 } else if (attribute.type == QInputMethodEvent::Cursor) {
1233 if (d->preEditStart < 0) {
1234 d->anchor = d->pos;
1235 } else {
1236 int index = d->shape->indexForPos(d->preEditStart);
1237 d->pos = d->shape->posForIndex(index + attribute.start);
1238 d->anchor = d->pos;
1239 }
1240
1241 // attribute value is the cursor color, and should be used to paint the cursor.
1242 // attribute length is about whether the cursor should be visible at all...
1243 }
1244 }
1245
1246 blocker.unlock();
1247 updateRect |= d->shape->boundingRect();
1248 // TODO: replace with KoShapeBulkActionLock
1249 d->shape->updateAbsolute(updateRect);
1250 d->styleMap = styleMap;
1253 updateCursor();
1254 event->accept();
1255}
1256
1258{
1259 if (d->shape) {
1260 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->cursorShape.boundingRect()) | d->oldCursorRect);
1261 d->cursorVisible = !d->cursorVisible;
1262 }
1263}
1264
1266{
1267 d->cursorFlash.stop();
1268 d->cursorFlashLimit.stop();
1269 d->cursorVisible = true;
1270 if (d->shape) {
1271 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->cursorShape.boundingRect()) | d->oldCursorRect);
1272 }
1273}
1274
1276{
1277 // Mockcanvas in the tests has no window.
1278 if (!d->canvas->canvasWidget()) {
1279 return;
1280 }
1281 QPoint pos = d->canvas->canvasWidget()->mapTo(d->canvas->canvasWidget()->window(), QPoint());
1282 QTransform widgetToWindow = QTransform::fromTranslate(pos.x(), pos.y());
1283 QTransform inputItemTransform = widgetToWindow;
1284 QRectF inputRect = d->canvas->canvasWidget()->geometry();
1285 if (d->shape) {
1286 inputRect = d->shape->outlineRect().normalized();
1287 QTransform shapeTransform = d->shape->absoluteTransformation();
1288 QTransform docToView = d->canvas->viewConverter()->documentToView();
1289 QTransform viewToWidget = d->canvas->viewConverter()->viewToWidget();
1290 inputItemTransform = shapeTransform * docToView * viewToWidget * widgetToWindow;
1291 // Only mess with IME if we're actually the thing being typed at.
1292 if (d->hasFocus) {
1293 QInputMethod *inputMethod = QGuiApplication::inputMethod();
1294 inputMethod->setInputItemTransform(inputItemTransform);
1295 inputMethod->setInputItemRectangle(inputRect);
1296 if (!d->blockQueryUpdates) {
1297 inputMethod->update(Qt::ImQueryInput);
1298 }
1299 }
1300 }
1301}
1302
1303void SvgTextCursor::canvasResourceChanged(int key, const QVariant &value)
1304{
1306 return;
1307
1308 KoSvgTextProperties props;
1309 KoSvgTextProperties shapeProps = hasSelection()? d->shape->propertiesForPos(qMin(d->pos, d->anchor), true): d->shape->textProperties();
1312 if (!bg->compareTo(shapeProps.background().data())
1313 || !shapeProps.hasProperty(KoSvgTextProperties::FillId)) {
1315 QVariant::fromValue(KoSvgText::BackgroundProperty(bg)));
1316 }
1317 } else if (key == KoCanvasResource::BackgroundColor) {
1320 KoShapeStrokeSP shapeStroke = qSharedPointerDynamicCast<KoShapeStroke>(shapeProps.stroke());
1321 if (shapeStroke->isVisible()) {
1322 stroke.reset(new KoShapeStroke(*shapeStroke));
1323 }
1324 }
1325 stroke->setColor(value.value<KoColor>().toQColor());
1326 if (!stroke->compareFillTo(shapeProps.stroke().data()) || !shapeProps.hasProperty(KoSvgTextProperties::StrokeId)) {
1328 QVariant::fromValue(KoSvgText::StrokeProperty(stroke)));
1329 }
1330 }
1331 if (!props.isEmpty()) {
1332 mergePropertiesIntoSelection(props, QSet<KoSvgTextProperties::PropertyId>(), !hasSelection());
1333 }
1334}
1335
1337{
1338 QAction *action = dynamic_cast<QAction*>(QObject::sender());
1339 if (!action || !d->shape) return;
1340
1341 const QList<KoSvgTextProperties> p = d->shape->propertiesForRange(qMin(d->pos, d->anchor), qMax(d->pos, d->anchor));
1343 if (properties.isEmpty()) return;
1344 mergePropertiesIntoSelection(properties);
1345}
1346
1348{
1349 KoSvgTextProperties props;
1350
1351 QSet<KoSvgTextProperties::PropertyId> ids;
1352
1353 for (int i = 0; i < int(KoSvgTextProperties::LastPropertyId); i++) {
1354 ids.insert(KoSvgTextProperties::PropertyId(i));
1355 }
1356
1357 mergePropertiesIntoSelection(props, ids);
1358}
1359
1361{
1362 return d->pos != d->anchor;
1363}
1364
1366{
1367 Q_UNUSED(type);
1368 Q_UNUSED(shape);
1369 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1370 d->pos = d->shape->posForIndex(d->posIndex);
1371 d->anchor = d->shape->posForIndex(d->anchorIndex);
1372 updateCursor(true);
1376}
1377
1379{
1380 Private::InputQueryUpdateBlocker inputQueryUpdateBlocker(d);
1381 d->pos = pos;
1382 d->anchor = anchor;
1383 updateCursor();
1386}
1387
1389{
1390 d->interface->emitSelectionChange();
1391 d->interface->emitCharacterSelectionChange();
1392 updateCursor();
1395}
1396
1397void SvgTextCursor::keyPressEvent(QKeyEvent *event)
1398{
1400
1401 updateModifiers(event->modifiers());
1402
1403 if (d->preEditCommand) {
1404 //MacOS will keep sending keyboard events during IME handling.
1405 event->accept();
1406 return;
1407 }
1408
1409 bool select = event->modifiers().testFlag(Qt::ShiftModifier);
1410
1411 if (!((Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier) & event->modifiers())) {
1412
1413 switch (event->key()) {
1414 case Qt::Key_Right:
1416 event->accept();
1417 break;
1418 case Qt::Key_Left:
1420 event->accept();
1421 break;
1422 case Qt::Key_Up:
1424 event->accept();
1425 break;
1426 case Qt::Key_Down:
1428 event->accept();
1429 break;
1430 case Qt::Key_Delete:
1432 event->accept();
1433 break;
1434 case Qt::Key_Backspace:
1436 event->accept();
1437 break;
1438 case Qt::Key_Return:
1439 case Qt::Key_Enter:
1440 insertText("\n");
1441 event->accept();
1442 break;
1443 default:
1444 event->ignore();
1445 }
1446
1447 if (event->isAccepted()) {
1448 return;
1449 }
1450 }
1451 if (acceptableInput(event)) {
1452 insertText(event->text());
1453 event->accept();
1454 return;
1455 }
1456
1457 KoSvgTextProperties props = d->shape->textProperties();
1458
1461
1462 // Qt's keysequence stuff doesn't handle vertical, so to test all the standard keyboard shortcuts as if it did,
1463 // we reinterpret the direction keys according to direction and writing mode, and test against that.
1464
1465 int newKey = event->key();
1466
1467 if (direction == KoSvgText::DirectionRightToLeft) {
1468 switch (newKey) {
1469 case Qt::Key_Left:
1470 newKey = Qt::Key_Right;
1471 break;
1472 case Qt::Key_Right:
1473 newKey = Qt::Key_Left;
1474 break;
1475 default:
1476 break;
1477 }
1478 }
1479
1480 if (mode == KoSvgText::VerticalRL) {
1481 switch (newKey) {
1482 case Qt::Key_Left:
1483 newKey = Qt::Key_Down;
1484 break;
1485 case Qt::Key_Right:
1486 newKey = Qt::Key_Up;
1487 break;
1488 case Qt::Key_Up:
1489 newKey = Qt::Key_Left;
1490 break;
1491 case Qt::Key_Down:
1492 newKey = Qt::Key_Right;
1493 break;
1494 default:
1495 break;
1496 }
1497 } else if (mode == KoSvgText::VerticalRL) {
1498 switch (newKey) {
1499 case Qt::Key_Left:
1500 newKey = Qt::Key_Up;
1501 break;
1502 case Qt::Key_Right:
1503 newKey = Qt::Key_Down;
1504 break;
1505 case Qt::Key_Up:
1506 newKey = Qt::Key_Left;
1507 break;
1508 case Qt::Key_Down:
1509 newKey = Qt::Key_Right;
1510 break;
1511 default:
1512 break;
1513 }
1514 }
1515
1516 QKeySequence testSequence(event->modifiers() | newKey);
1517
1518
1519 // Note for future, when we have format changing actions:
1520 // We'll need to test format change actions before the standard
1521 // keys, as one of the standard keys for deleting a line is ctrl+u
1522 // which would probably be expected to do underline before deleting.
1523
1524 Q_FOREACH(QAction *action, d->actions) {
1525 if (action->shortcut() == testSequence) {
1526 event->accept();
1527 action->trigger();
1528 return;
1529 }
1530 }
1531
1532 // This first set is already tested above, however, if they still
1533 // match, then it's one of the extra sequences for MacOs, which
1534 // seem to be purely logical, instead of the visual set we tested
1535 // above.
1536 if (testSequence == QKeySequence::MoveToNextChar) {
1538 event->accept();
1539 } else if (testSequence == QKeySequence::SelectNextChar) {
1541 event->accept();
1542 } else if (testSequence == QKeySequence::MoveToPreviousChar) {
1544 event->accept();
1545 } else if (testSequence == QKeySequence::SelectPreviousChar) {
1547 event->accept();
1548 } else if (testSequence == QKeySequence::MoveToNextLine) {
1550 event->accept();
1551 } else if (testSequence == QKeySequence::SelectNextLine) {
1553 event->accept();
1554 } else if (testSequence == QKeySequence::MoveToPreviousLine) {
1556 event->accept();
1557 } else if (testSequence == QKeySequence::SelectPreviousLine) {
1559 event->accept();
1560
1561 } else if (testSequence == QKeySequence::MoveToNextWord) {
1563 event->accept();
1564 } else if (testSequence == QKeySequence::SelectNextWord) {
1566 event->accept();
1567 } else if (testSequence == QKeySequence::MoveToPreviousWord) {
1569 event->accept();
1570 } else if (testSequence == QKeySequence::SelectPreviousWord) {
1572 event->accept();
1573
1574 } else if (testSequence == QKeySequence::MoveToStartOfLine) {
1576 event->accept();
1577 } else if (testSequence == QKeySequence::SelectStartOfLine) {
1579 event->accept();
1580 } else if (testSequence == QKeySequence::MoveToEndOfLine) {
1582 event->accept();
1583 } else if (testSequence == QKeySequence::SelectEndOfLine) {
1585 event->accept();
1586
1587 } else if (testSequence == QKeySequence::MoveToStartOfBlock
1588 || testSequence == QKeySequence::MoveToStartOfDocument) {
1590 event->accept();
1591 } else if (testSequence == QKeySequence::SelectStartOfBlock
1592 || testSequence == QKeySequence::SelectStartOfDocument) {
1594 event->accept();
1595
1596 } else if (testSequence == QKeySequence::MoveToEndOfBlock
1597 || testSequence == QKeySequence::MoveToEndOfDocument) {
1599 event->accept();
1600 } else if (testSequence == QKeySequence::SelectEndOfBlock
1601 || testSequence == QKeySequence::SelectEndOfDocument) {
1603 event->accept();
1604
1605 }else if (testSequence == QKeySequence::DeleteStartOfWord) {
1607 event->accept();
1608 } else if (testSequence == QKeySequence::DeleteEndOfWord) {
1610 event->accept();
1611 } else if (testSequence == QKeySequence::DeleteEndOfLine) {
1613 event->accept();
1614 } else if (testSequence == QKeySequence::DeleteCompleteLine) {
1616 event->accept();
1617 } else if (testSequence == QKeySequence::Backspace) {
1619 event->accept();
1620 } else if (testSequence == QKeySequence::Delete) {
1622 event->accept();
1623
1624 } else if (testSequence == QKeySequence::InsertLineSeparator
1625 || testSequence == QKeySequence::InsertParagraphSeparator) {
1626 insertText("\n");
1627 event->accept();
1628 } else {
1629 event->ignore();
1630 }
1631}
1632
1633void SvgTextCursor::updateModifiers(const Qt::KeyboardModifiers modifiers)
1634{
1635 d->lastKnownModifiers = modifiers;
1637}
1638
1640{
1641 return d->isAddingCommand;
1642}
1643
1645{
1646 d->cursorFlash.start();
1647 d->cursorFlashLimit.start();
1648 d->cursorVisible = false;
1649 d->hasFocus = true;
1650 blinkCursor();
1651}
1652
1654{
1655 d->hasFocus = false;
1657}
1658
1659bool SvgTextCursor::registerPropertyAction(QAction *action, const QString &name)
1660{
1661 if (SvgTextShortCuts::configureAction(action, name)) {
1662 d->actions.append(action);
1663 connect(action, SIGNAL(triggered(bool)), this, SLOT(propertyAction()));
1664 return true;
1665 } else if (name == "svg_insert_special_character") {
1666 d->actions.append(action);
1667 connect(action, SIGNAL(triggered(bool)), this, SIGNAL(sigOpenGlyphPalette()));
1668 return true;
1669 } else if (name == "svg_paste_rich_text") {
1670 d->actions.append(action);
1671 connect(action, SIGNAL(triggered(bool)), this, SLOT(pasteRichText()));
1672 return true;
1673 } else if (name == "svg_paste_plain_text") {
1674 d->actions.append(action);
1675 connect(action, SIGNAL(triggered(bool)), this, SLOT(pastePlainText()));
1676 return true;
1677 } else if (name == "svg_remove_transforms_from_range") {
1678 d->actions.append(action);
1679 connect(action, SIGNAL(triggered(bool)), this, SLOT(removeTransformsFromRange()));
1680 return true;
1681 } else if (name == "svg_clear_formatting") {
1682 d->actions.append(action);
1683 connect(action, SIGNAL(triggered(bool)), this, SLOT(clearFormattingAction()));
1684 return true;
1685 } else if (action) {
1686 d->actions.append(action);
1687 return true;
1688 }
1689 return false;
1690}
1691
1696
1697void SvgTextCursor::updateCursor(bool firstUpdate)
1698{
1699 if (d->shape) {
1700 d->oldCursorRect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1701 d->posIndex = d->shape->indexForPos(d->pos);
1702 d->anchorIndex = d->shape->indexForPos(d->anchor);
1703 emit selectionChanged();
1705 }
1706 d->cursorColor = QColor();
1707 d->cursorShape = d->shape? d->shape->cursorForPos(d->pos, d->cursorCaret, d->cursorColor): QPainterPath();
1708
1709 if (!d->blockQueryUpdates) {
1710 qApp->inputMethod()->update(Qt::ImQueryInput);
1711 }
1712 d->interface->emitCharacterSelectionChange();
1713 if (!(d->canvas->canvasWidget() && d->canvas->canvasController())) {
1714 // Mockcanvas in the tests has neither.
1715 return;
1716 }
1717 if (d->shape && !firstUpdate) {
1718 QRectF rect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1719 d->canvas->canvasController()->ensureVisibleDoc(rect, false);
1720 }
1721 if (d->canvas->canvasWidget()->hasFocus()) {
1722 d->cursorFlash.start();
1723 d->cursorFlashLimit.start();
1724 d->cursorVisible = false;
1725 blinkCursor();
1726 }
1727}
1728
1730{
1731 if (d->shape) {
1732 d->oldSelectionRect = d->shape->shapeToDocument(d->selection.boundingRect());
1733 d->shape->cursorForPos(d->anchor, d->anchorCaret, d->cursorColor);
1734 d->selection = d->shape->selectionBoxes(d->pos, d->anchor);
1735 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->selection.boundingRect()) | d->oldSelectionRect);
1736
1737 if (!d->blockQueryUpdates) {
1738 QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
1739 }
1740 }
1741}
1742
1744{
1745 if (d->shape) {
1746 d->oldIMEDecorationRect = d->shape->shapeToDocument(d->IMEDecoration.boundingRect());
1747 KoSvgText::TextDecorations decor;
1748 decor.setFlag(KoSvgText::DecorationUnderline, true);
1749 d->IMEDecoration = QPainterPath();
1750 if (d->preEditCommand) {
1751 Q_FOREACH(const IMEDecorationInfo info, d->styleMap) {
1752
1753 int startIndex = d->shape->indexForPos(d->preEditStart) + info.start;
1754 int endIndex = startIndex + info.length;
1755 qreal minimum = d->canvas->viewToDocument(QPointF(1, 1)).x();
1756 d->IMEDecoration.addPath(d->shape->underlines(d->shape->posForIndex(startIndex),
1757 d->shape->posForIndex(endIndex),
1758 info.decor,
1759 info.style,
1760 minimum,
1761 info.thick));
1762 d->IMEDecoration.setFillRule(Qt::WindingFill);
1763 }
1764 }
1765
1766 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->IMEDecoration.boundingRect()) | d->oldIMEDecorationRect);
1767 }
1768}
1769
1772int calcLineHeight(const KoSvgText::LineHeightInfo &lineHeight, const KoSvgText::FontMetrics &metrics, const qreal scaleMetrics) {
1773 if (!lineHeight.isNormal) {
1774 if (lineHeight.isNumber) {
1775 return (lineHeight.value * (metrics.ascender-metrics.descender))-(metrics.ascender-metrics.descender);
1776 } else {
1777 return (lineHeight.length.value/scaleMetrics)-(metrics.ascender-metrics.descender);
1778 }
1779 }
1780 return metrics.lineGap;
1781}
1782
1784 const int metric, const bool isHorizontal,
1785 QTransform t,
1786 const qreal scaleMetrics, const QPointF &advance,
1787 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> &decor){
1788 QPointF offset = isHorizontal? QPointF(0, -(metric*scaleMetrics)): QPointF(metric*scaleMetrics, 0);
1789
1790 QPainterPath p = decor.value(handle);
1791
1792 const QPointF startPos = t.map(offset);
1793 const QPointF endPos = t.map(offset+advance);
1794 if (p.currentPosition() != startPos) {
1795 p.moveTo(startPos);
1796 }
1797 p.lineTo(endPos);
1798
1799 decor.insert(handle, p);
1800}
1801
1802void processEdges(QTransform t, QMap<SvgTextCursor::TypeSettingModeHandle, int> values,
1803 const bool isHorizontal,
1804 const qreal scaleMetrics,
1805 const QPointF advance,
1806 QPainterPath &path) {
1807 QPointF p1(values.first(), 0);
1808 QPointF p2(values.last(), 0);
1809 Q_FOREACH(const int val, values) {
1810 if (val < p1.x()) {
1811 p1.setX(val);
1812 }
1813 if (val > p2.x()) {
1814 p2.setX(val);
1815 }
1816 }
1817 if (isHorizontal) {
1818 p1 = QPointF(p1.y(), -p1.x());
1819 p2 = QPointF(p2.y(), -p2.x());
1820 }
1821 p1 *= scaleMetrics;
1822 p2 *= scaleMetrics;
1823 path.moveTo(t.map(p1+advance));
1824 path.lineTo(t.map(p2+advance));
1825}
1826
1827QTransform posAndRotateTransform(const QPointF pos, const qreal rotateDeg) {
1828 QTransform t = QTransform::fromTranslate(pos.x(), pos.y());
1829 t.rotate(rotateDeg);
1830 return t;
1831}
1832
1834{
1835 QRectF updateRect;
1836 if (d->shape && d->typeSettingMode) {
1837
1839 d->shape->getPositionsAndRotationsForRange(d->pos, d->anchor);
1840 if (infos.size() < 1) return;
1841
1842 const bool rtl = infos.first().rtl;
1843
1844 KoSvgTextCharacterInfo first = infos.first();
1845 KoSvgTextCharacterInfo last = infos.last();
1846 if (infos.size() > 1) {
1847 std::sort(infos.begin(), infos.end(), KoSvgTextCharacterInfo::visualLessThan);
1848 for (auto it = infos.begin(); it != infos.end(); it++) {
1849 if (it->visualIndex >= 0) {
1850 first = *it;
1851 break;
1852 }
1853 }
1854 for (auto it = infos.rbegin(); it != infos.rend(); it++) {
1855 if (it->visualIndex >= 0) {
1856 last = *it;
1857 break;
1858 }
1859 }
1860 }
1861
1862 QTransform t = QTransform::fromTranslate(last.finalPos.x(), last.finalPos.y());
1863 t.rotate(last.rotateDeg);
1864 last.finalPos = t.map(last.advance);
1865
1866 d->typeSettingDecor.handles.first = rtl? last.finalPos: first.finalPos;
1867 d->typeSettingDecor.handles.second = rtl? first.finalPos: last.finalPos;
1868
1869 //Start collecting the metrics decoration...
1870 d->typeSettingDecor.paths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1871 d->typeSettingDecor.baselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1872 d->typeSettingDecor.edges = QPainterPath();
1873 d->typeSettingDecor.parentPaths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1874 d->typeSettingDecor.parentBaselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1875 QList<KoSvgTextCharacterInfo> metricInfos = infos;
1876 if (d->pos == d->anchor) {
1877 metricInfos = d->shape->getPositionsAndRotationsForRange(0, d->shape->posForIndex(d->shape->plainText().size()));
1878 }
1879 const bool isHorizontal = d->shape->writingMode() == KoSvgText::HorizontalTB;
1880 const int minPos = qMin(d->pos, d->anchor);
1881 const int maxPos = qMax(d->pos, d->anchor);
1882 const int endPos = d->shape->posForIndex(d->shape->plainText().size());
1883
1885 if (minPos > 0 || maxPos < endPos) {
1886 const KoSvgTextProperties props = d->shape->textProperties();
1888 KoSvgText::FontMetrics metrics = props.metrics(true, true);
1889 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1890 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1891
1892 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1893 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1894
1896 QList<int> positions;
1897 positions << 0;
1898 positions << qMax(0, minPos-1);
1899 positions << maxPos;
1900 positions << endPos;
1901
1902 Q_FOREACH(const int pos, positions) {
1904 d->shape->getPositionsAndRotationsForRange(pos, pos);
1905 parentInfos.append(info.first());
1906 }
1907
1908 QPointF drawOffset = isHorizontal? QPointF(d->handleRadius*2, 0): QPointF(0, d->handleRadius*2);
1909 if (d->canvas) {
1910 drawOffset = d->canvas->viewConverter()->viewToDocument().map(drawOffset);
1911 }
1912 bool toggleOffset = rtl;
1913
1914 for (auto it = parentInfos.begin(); it != parentInfos.end(); it++) {
1915 const bool currentIsMin = (d->shape->posForIndex(it->logicalIndex) == minPos);
1916 const bool currentIsMax = (d->shape->posForIndex(it->logicalIndex) == maxPos);
1917
1918 if ((currentIsMin && minPos == 0) || (currentIsMax && maxPos == endPos)) {
1919 toggleOffset = !toggleOffset;
1920 continue;
1921 }
1922
1923 const QPointF finalPos = toggleOffset? it->finalPos + (it->advance - drawOffset): it->finalPos;
1924 const QPointF advance = drawOffset;
1925
1926 const QTransform t = posAndRotateTransform(finalPos, it->rotateDeg);
1927
1928 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1929 const int metric = types.value(handle);
1930 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, advance, d->typeSettingDecor.parentBaselines);
1931 }
1932
1933 {
1934 if (currentIsMin && minPos == maxPos && !toggleOffset) {
1935 toggleOffset = !toggleOffset;
1936 continue;
1937 }
1938 const QPointF advance = toggleOffset? it->advance: QPointF();
1939 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1940 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1941 }
1942
1943 toggleOffset = !toggleOffset;
1944 }
1945 }
1946
1948 bool toggleOffset = false; // MetricInfos are visually-ordered.
1949 for (auto it = metricInfos.begin(); it != metricInfos.end(); it++) {
1950 const int currentPos = (d->pos == d->anchor)? -1 :d->shape->posForIndex(it->logicalIndex);
1951
1952 KoSvgTextProperties props = d->shape->propertiesForPos(currentPos, true);
1954 KoSvgText::FontMetrics propMetrics = props.metrics(true, true);
1955
1956 KoSvgText::FontMetrics metrics = (d->pos == d->anchor)? propMetrics :it->metrics;
1957
1958 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1959 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1960
1961 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1962
1963 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1964 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1965
1966 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1967 const int metric = types.value(handle);
1968 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, it->advance, d->typeSettingDecor.baselines);
1969 }
1970 if ((currentPos == minPos || currentPos+1 == maxPos) && minPos != maxPos) {
1971 const QPointF advance = toggleOffset? it->advance: QPointF();
1972 toggleOffset = !toggleOffset;
1973 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1974 }
1975 }
1976
1978 const QList<SvgTextCursor::TypeSettingModeHandle> nonBaselines = {
1980 };
1981 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, nonBaselines) {
1982 d->typeSettingDecor.paths.insert(handle, d->typeSettingDecor.baselines.value(handle));
1983 d->typeSettingDecor.parentPaths.insert(handle, d->typeSettingDecor.parentBaselines.value(handle));
1984 }
1985 d->typeSettingDecor.baselines.remove(LineHeightTop);
1986 d->typeSettingDecor.baselines.remove(LineHeightBottom);
1987 d->typeSettingDecor.parentBaselines.remove(LineHeightTop);
1988 d->typeSettingDecor.parentBaselines.remove(LineHeightBottom);
1989
1990 updateRect = d->shape->shapeToDocument(d->typeSettingDecor.boundingRect(d->handleRadius));
1991 }
1992 updateTypeSettingDecorFromShape(); // To remove the text-in-path nodes..
1993 Q_EMIT updateCursorDecoration(updateRect | d->oldTypeSettingRect);
1994 d->oldTypeSettingRect = updateRect;
1995}
1996
1998{
1999 if (d->canvas) {
2000 if (cmd) {
2001 d->isAddingCommand = true;
2002 d->canvas->addCommand(cmd);
2003 d->isAddingCommand = false;
2004 }
2005 }
2006}
2007
2008int SvgTextCursor::moveModeResult(const SvgTextCursor::MoveMode mode, int &pos, bool visual) const
2009{
2010 int newPos = pos;
2011 switch (mode) {
2012 case MoveNone:
2013 break;
2014 case MoveLeft:
2015 newPos = d->shape->posLeft(pos, visual);
2016 break;
2017 case MoveRight:
2018 newPos = d->shape->posRight(pos, visual);
2019 break;
2020 case MoveUp:
2021 newPos = d->shape->posUp(pos, visual);
2022 break;
2023 case MoveDown:
2024 newPos = d->shape->posDown(pos, visual);
2025 break;
2026 case MovePreviousChar:
2027 newPos = d->shape->previousIndex(pos);
2028 break;
2029 case MoveNextChar:
2030 newPos = d->shape->nextIndex(pos);
2031 break;
2032 case MovePreviousLine:
2033 newPos = d->shape->previousLine(pos);
2034 break;
2035 case MoveNextLine:
2036 newPos = d->shape->nextLine(pos);
2037 break;
2038 case MoveWordLeft:
2039 newPos = d->shape->wordLeft(pos, visual);
2040 if (newPos == pos) {
2041 newPos = d->shape->posLeft(pos, visual);
2042 newPos = d->shape->wordLeft(newPos, visual);
2043 }
2044 break;
2045 case MoveWordRight:
2046 newPos = d->shape->wordRight(pos, visual);
2047 if (newPos == pos) {
2048 newPos = d->shape->posRight(pos, visual);
2049 newPos = d->shape->wordRight(newPos, visual);
2050 }
2051 break;
2052 case MoveWordStart:
2053 newPos = d->shape->wordStart(pos);
2054 if (newPos == pos) {
2055 newPos = d->shape->previousIndex(pos);
2056 newPos = d->shape->wordStart(newPos);
2057 }
2058 break;
2059 case MoveWordEnd:
2060 newPos = d->shape->wordEnd(pos);
2061 if (newPos == pos) {
2062 newPos = d->shape->nextIndex(pos);
2063 newPos = d->shape->wordEnd(newPos);
2064 }
2065 break;
2066 case MoveLineStart:
2067 newPos = d->shape->lineStart(pos);
2068 break;
2069 case MoveLineEnd:
2070 newPos = d->shape->lineEnd(pos);
2071 break;
2072 case ParagraphStart:
2073 newPos = 0;
2074 break;
2075 case ParagraphEnd:
2076 newPos = d->shape->posForIndex(d->shape->plainText().size());
2077 break;
2078 }
2079 return newPos;
2080}
2081
2083bool SvgTextCursor::acceptableInput(const QKeyEvent *event) const
2084{
2085 const QString text = event->text();
2086 if (text.isEmpty())
2087 return false;
2088 const QChar c = text.at(0);
2089 // Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the
2090 // next test, since CTRL+SHIFT is sometimes used to input it on Windows.
2091 if (c.category() == QChar::Other_Format)
2092 return true;
2093 // QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards
2094 if (event->modifiers() == Qt::ControlModifier
2095 || event->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
2096 return false;
2097 }
2098 if (c.isPrint())
2099 return true;
2100 if (c.category() == QChar::Other_PrivateUse)
2101 return true;
2102 if (c == QLatin1Char('\t'))
2103 return true;
2104 return false;
2105}
2106
2108{
2109 if (!d->preEditCommand) {
2110 return;
2111 }
2112
2113 qApp->inputMethod()->commit();
2114
2115 if (!d->preEditCommand) {
2116 return;
2117 }
2118
2119 d->preEditCommand->undo();
2120 d->preEditCommand = nullptr;
2121 d->preEditStart = -1;
2122 d->preEditLength = 0;
2124 updateCursor();
2125}
2126
2128{
2129 // Only update canvas resources when there's no selection.
2130 // This relies on Krita not setting anything on the text when there's no selection.
2131 if (d->shape && d->canvas->resourceManager() && d->pos == d->anchor) {
2132 KoSvgTextProperties props = hasSelection()? d->shape->propertiesForPos(qMin(d->pos, d->anchor), true): d->shape->textProperties();
2133 KoColorBackground *bg = dynamic_cast<KoColorBackground *>(props.background().data());
2134 if (bg && props.hasProperty(KoSvgTextProperties::FillId)) {
2135 KoColor c;
2136 c.fromQColor(bg->color());
2137 c.setOpacity(1.0);
2138 if (c != d->canvas->resourceManager()->foregroundColor()) {
2139 d->canvas->resourceManager()->setForegroundColor(c);
2140 }
2141 }
2142 KoShapeStroke *stroke = dynamic_cast<KoShapeStroke *>(props.stroke().data());
2143 if (stroke && stroke->isVisible() && stroke->color().isValid() && props.hasProperty(KoSvgTextProperties::StrokeId)) {
2144 KoColor c;
2145 c.fromQColor(stroke->color());
2146 c.setOpacity(1.0);
2147 if (c != d->canvas->resourceManager()->backgroundColor()) {
2148 d->canvas->resourceManager()->setBackgroundColor(c);
2149 }
2150 }
2151
2152 Q_FOREACH (QAction *action, d->actions) {
2153 // Blocking signals so that we don't get a toggle action while evaluating the checked-ness.
2154 action->blockSignals(true);
2155 const QList<KoSvgTextProperties> r = d->shape->propertiesForRange(qMin(d->pos, d->anchor), qMax(d->pos, d->anchor), true);
2156 if (action->isCheckable() && SvgTextShortCuts::possibleActions().contains(action->objectName())) {
2157 const bool checked = SvgTextShortCuts::actionEnabled(action, r);
2158 if (action->isChecked() != checked) {
2159 action->setChecked(checked);
2160 }
2161 }
2162 action->blockSignals(false);
2163 }
2164 }
2165}
2166
2176
2178 : KoSvgTextPropertiesInterface(parent), d(new Private(parent))
2179{
2180 connect(&d->compressor, SIGNAL(timeout()), this, SIGNAL(textSelectionChanged()));
2181 connect(&d->characterCompressor, SIGNAL(timeout()), this, SIGNAL(textCharacterSelectionChanged()));
2182}
2183
2189{
2190 return d->parent->propertiesForShape();
2191}
2192
2194{
2195 // When there's only a single node, its best to only return empty properties, as paragraph properties handle that single node.
2196 if (!d->parent->shape()) return QList<KoSvgTextProperties>();
2197 if (d->parent->shape()->singleNode()) {
2198 return {KoSvgTextProperties()};
2199 }
2200 return d->parent->propertiesForRange();
2201}
2202
2204{
2205 // 9 times out of 10 this is correct, though we could do better by actually
2206 // getting inherited properties for the range and not just defaulting to the paragraph.
2207 return (d->parent->shape())? d->parent->shape()->textProperties(): KoSvgTextProperties();
2208}
2209
2210void SvgTextCursorPropertyInterface::setPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2211{
2212 d->parent->mergePropertiesIntoSelection(properties, removeProperties, true);
2213}
2214
2215void SvgTextCursorPropertyInterface::setCharacterPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2216{
2217 d->parent->mergePropertiesIntoSelection(properties, removeProperties, false, true);
2218}
2219
2221{
2222 return d->parent->hasSelection();
2223}
2224
2229
2231{
2232 // Don't bother updating the selection when there's no shape
2233 // this is so we can use the text properties last used to create new texts.
2234 if (!d->parent->shape()) return;
2235 d->compressor.start();
2236}
2237
2239{
2240 d->characterCompressor.start();
2241}
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:1157
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)