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();
1393}
1394
1395void SvgTextCursor::keyPressEvent(QKeyEvent *event)
1396{
1398
1399 updateModifiers(event->modifiers());
1400
1401 if (d->preEditCommand) {
1402 //MacOS will keep sending keyboard events during IME handling.
1403 event->accept();
1404 return;
1405 }
1406
1407 bool select = event->modifiers().testFlag(Qt::ShiftModifier);
1408
1409 if (!((Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier) & event->modifiers())) {
1410
1411 switch (event->key()) {
1412 case Qt::Key_Right:
1414 event->accept();
1415 break;
1416 case Qt::Key_Left:
1418 event->accept();
1419 break;
1420 case Qt::Key_Up:
1422 event->accept();
1423 break;
1424 case Qt::Key_Down:
1426 event->accept();
1427 break;
1428 case Qt::Key_Delete:
1430 event->accept();
1431 break;
1432 case Qt::Key_Backspace:
1434 event->accept();
1435 break;
1436 case Qt::Key_Return:
1437 case Qt::Key_Enter:
1438 insertText("\n");
1439 event->accept();
1440 break;
1441 default:
1442 event->ignore();
1443 }
1444
1445 if (event->isAccepted()) {
1446 return;
1447 }
1448 }
1449 if (acceptableInput(event)) {
1450 insertText(event->text());
1451 event->accept();
1452 return;
1453 }
1454
1455 KoSvgTextProperties props = d->shape->textProperties();
1456
1459
1460 // Qt's keysequence stuff doesn't handle vertical, so to test all the standard keyboard shortcuts as if it did,
1461 // we reinterpret the direction keys according to direction and writing mode, and test against that.
1462
1463 int newKey = event->key();
1464
1465 if (direction == KoSvgText::DirectionRightToLeft) {
1466 switch (newKey) {
1467 case Qt::Key_Left:
1468 newKey = Qt::Key_Right;
1469 break;
1470 case Qt::Key_Right:
1471 newKey = Qt::Key_Left;
1472 break;
1473 default:
1474 break;
1475 }
1476 }
1477
1478 if (mode == KoSvgText::VerticalRL) {
1479 switch (newKey) {
1480 case Qt::Key_Left:
1481 newKey = Qt::Key_Down;
1482 break;
1483 case Qt::Key_Right:
1484 newKey = Qt::Key_Up;
1485 break;
1486 case Qt::Key_Up:
1487 newKey = Qt::Key_Left;
1488 break;
1489 case Qt::Key_Down:
1490 newKey = Qt::Key_Right;
1491 break;
1492 default:
1493 break;
1494 }
1495 } else if (mode == KoSvgText::VerticalRL) {
1496 switch (newKey) {
1497 case Qt::Key_Left:
1498 newKey = Qt::Key_Up;
1499 break;
1500 case Qt::Key_Right:
1501 newKey = Qt::Key_Down;
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 }
1513
1514 QKeySequence testSequence(event->modifiers() | newKey);
1515
1516
1517 // Note for future, when we have format changing actions:
1518 // We'll need to test format change actions before the standard
1519 // keys, as one of the standard keys for deleting a line is ctrl+u
1520 // which would probably be expected to do underline before deleting.
1521
1522 Q_FOREACH(QAction *action, d->actions) {
1523 if (action->shortcut() == testSequence) {
1524 event->accept();
1525 action->trigger();
1526 return;
1527 }
1528 }
1529
1530 // This first set is already tested above, however, if they still
1531 // match, then it's one of the extra sequences for MacOs, which
1532 // seem to be purely logical, instead of the visual set we tested
1533 // above.
1534 if (testSequence == QKeySequence::MoveToNextChar) {
1536 event->accept();
1537 } else if (testSequence == QKeySequence::SelectNextChar) {
1539 event->accept();
1540 } else if (testSequence == QKeySequence::MoveToPreviousChar) {
1542 event->accept();
1543 } else if (testSequence == QKeySequence::SelectPreviousChar) {
1545 event->accept();
1546 } else if (testSequence == QKeySequence::MoveToNextLine) {
1548 event->accept();
1549 } else if (testSequence == QKeySequence::SelectNextLine) {
1551 event->accept();
1552 } else if (testSequence == QKeySequence::MoveToPreviousLine) {
1554 event->accept();
1555 } else if (testSequence == QKeySequence::SelectPreviousLine) {
1557 event->accept();
1558
1559 } else if (testSequence == QKeySequence::MoveToNextWord) {
1561 event->accept();
1562 } else if (testSequence == QKeySequence::SelectNextWord) {
1564 event->accept();
1565 } else if (testSequence == QKeySequence::MoveToPreviousWord) {
1567 event->accept();
1568 } else if (testSequence == QKeySequence::SelectPreviousWord) {
1570 event->accept();
1571
1572 } else if (testSequence == QKeySequence::MoveToStartOfLine) {
1574 event->accept();
1575 } else if (testSequence == QKeySequence::SelectStartOfLine) {
1577 event->accept();
1578 } else if (testSequence == QKeySequence::MoveToEndOfLine) {
1580 event->accept();
1581 } else if (testSequence == QKeySequence::SelectEndOfLine) {
1583 event->accept();
1584
1585 } else if (testSequence == QKeySequence::MoveToStartOfBlock
1586 || testSequence == QKeySequence::MoveToStartOfDocument) {
1588 event->accept();
1589 } else if (testSequence == QKeySequence::SelectStartOfBlock
1590 || testSequence == QKeySequence::SelectStartOfDocument) {
1592 event->accept();
1593
1594 } else if (testSequence == QKeySequence::MoveToEndOfBlock
1595 || testSequence == QKeySequence::MoveToEndOfDocument) {
1597 event->accept();
1598 } else if (testSequence == QKeySequence::SelectEndOfBlock
1599 || testSequence == QKeySequence::SelectEndOfDocument) {
1601 event->accept();
1602
1603 }else if (testSequence == QKeySequence::DeleteStartOfWord) {
1605 event->accept();
1606 } else if (testSequence == QKeySequence::DeleteEndOfWord) {
1608 event->accept();
1609 } else if (testSequence == QKeySequence::DeleteEndOfLine) {
1611 event->accept();
1612 } else if (testSequence == QKeySequence::DeleteCompleteLine) {
1614 event->accept();
1615 } else if (testSequence == QKeySequence::Backspace) {
1617 event->accept();
1618 } else if (testSequence == QKeySequence::Delete) {
1620 event->accept();
1621
1622 } else if (testSequence == QKeySequence::InsertLineSeparator
1623 || testSequence == QKeySequence::InsertParagraphSeparator) {
1624 insertText("\n");
1625 event->accept();
1626 } else {
1627 event->ignore();
1628 }
1629}
1630
1631void SvgTextCursor::updateModifiers(const Qt::KeyboardModifiers modifiers)
1632{
1633 d->lastKnownModifiers = modifiers;
1635}
1636
1638{
1639 return d->isAddingCommand;
1640}
1641
1643{
1644 d->cursorFlash.start();
1645 d->cursorFlashLimit.start();
1646 d->cursorVisible = false;
1647 d->hasFocus = true;
1648 blinkCursor();
1649}
1650
1652{
1653 d->hasFocus = false;
1655}
1656
1657bool SvgTextCursor::registerPropertyAction(QAction *action, const QString &name)
1658{
1659 if (SvgTextShortCuts::configureAction(action, name)) {
1660 d->actions.append(action);
1661 connect(action, SIGNAL(triggered(bool)), this, SLOT(propertyAction()));
1662 return true;
1663 } else if (name == "svg_insert_special_character") {
1664 d->actions.append(action);
1665 connect(action, SIGNAL(triggered(bool)), this, SIGNAL(sigOpenGlyphPalette()));
1666 return true;
1667 } else if (name == "svg_paste_rich_text") {
1668 d->actions.append(action);
1669 connect(action, SIGNAL(triggered(bool)), this, SLOT(pasteRichText()));
1670 return true;
1671 } else if (name == "svg_paste_plain_text") {
1672 d->actions.append(action);
1673 connect(action, SIGNAL(triggered(bool)), this, SLOT(pastePlainText()));
1674 return true;
1675 } else if (name == "svg_remove_transforms_from_range") {
1676 d->actions.append(action);
1677 connect(action, SIGNAL(triggered(bool)), this, SLOT(removeTransformsFromRange()));
1678 return true;
1679 } else if (name == "svg_clear_formatting") {
1680 d->actions.append(action);
1681 connect(action, SIGNAL(triggered(bool)), this, SLOT(clearFormattingAction()));
1682 return true;
1683 } else if (action) {
1684 d->actions.append(action);
1685 return true;
1686 }
1687 return false;
1688}
1689
1694
1695void SvgTextCursor::updateCursor(bool firstUpdate)
1696{
1697 if (d->shape) {
1698 d->oldCursorRect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1699 d->posIndex = d->shape->indexForPos(d->pos);
1700 d->anchorIndex = d->shape->indexForPos(d->anchor);
1701 emit selectionChanged();
1703 }
1704 d->cursorColor = QColor();
1705 d->cursorShape = d->shape? d->shape->cursorForPos(d->pos, d->cursorCaret, d->cursorColor): QPainterPath();
1706
1707 if (!d->blockQueryUpdates) {
1708 qApp->inputMethod()->update(Qt::ImQueryInput);
1709 }
1710 d->interface->emitCharacterSelectionChange();
1711 if (!(d->canvas->canvasWidget() && d->canvas->canvasController())) {
1712 // Mockcanvas in the tests has neither.
1713 return;
1714 }
1715 if (d->shape && !firstUpdate) {
1716 QRectF rect = d->shape->shapeToDocument(d->cursorShape.boundingRect());
1717 d->canvas->canvasController()->ensureVisibleDoc(rect, false);
1718 }
1719 if (d->canvas->canvasWidget()->hasFocus()) {
1720 d->cursorFlash.start();
1721 d->cursorFlashLimit.start();
1722 d->cursorVisible = false;
1723 blinkCursor();
1724 }
1725}
1726
1728{
1729 if (d->shape) {
1730 d->oldSelectionRect = d->shape->shapeToDocument(d->selection.boundingRect());
1731 d->shape->cursorForPos(d->anchor, d->anchorCaret, d->cursorColor);
1732 d->selection = d->shape->selectionBoxes(d->pos, d->anchor);
1733 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->selection.boundingRect()) | d->oldSelectionRect);
1734
1735 if (!d->blockQueryUpdates) {
1736 QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
1737 }
1738 }
1739}
1740
1742{
1743 if (d->shape) {
1744 d->oldIMEDecorationRect = d->shape->shapeToDocument(d->IMEDecoration.boundingRect());
1745 KoSvgText::TextDecorations decor;
1746 decor.setFlag(KoSvgText::DecorationUnderline, true);
1747 d->IMEDecoration = QPainterPath();
1748 if (d->preEditCommand) {
1749 Q_FOREACH(const IMEDecorationInfo info, d->styleMap) {
1750
1751 int startIndex = d->shape->indexForPos(d->preEditStart) + info.start;
1752 int endIndex = startIndex + info.length;
1753 qreal minimum = d->canvas->viewToDocument(QPointF(1, 1)).x();
1754 d->IMEDecoration.addPath(d->shape->underlines(d->shape->posForIndex(startIndex),
1755 d->shape->posForIndex(endIndex),
1756 info.decor,
1757 info.style,
1758 minimum,
1759 info.thick));
1760 d->IMEDecoration.setFillRule(Qt::WindingFill);
1761 }
1762 }
1763
1764 Q_EMIT updateCursorDecoration(d->shape->shapeToDocument(d->IMEDecoration.boundingRect()) | d->oldIMEDecorationRect);
1765 }
1766}
1767
1770int calcLineHeight(const KoSvgText::LineHeightInfo &lineHeight, const KoSvgText::FontMetrics &metrics, const qreal scaleMetrics) {
1771 if (!lineHeight.isNormal) {
1772 if (lineHeight.isNumber) {
1773 return (lineHeight.value * (metrics.ascender-metrics.descender))-(metrics.ascender-metrics.descender);
1774 } else {
1775 return (lineHeight.length.value/scaleMetrics)-(metrics.ascender-metrics.descender);
1776 }
1777 }
1778 return metrics.lineGap;
1779}
1780
1782 const int metric, const bool isHorizontal,
1783 QTransform t,
1784 const qreal scaleMetrics, const QPointF &advance,
1785 QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath> &decor){
1786 QPointF offset = isHorizontal? QPointF(0, -(metric*scaleMetrics)): QPointF(metric*scaleMetrics, 0);
1787
1788 QPainterPath p = decor.value(handle);
1789
1790 const QPointF startPos = t.map(offset);
1791 const QPointF endPos = t.map(offset+advance);
1792 if (p.currentPosition() != startPos) {
1793 p.moveTo(startPos);
1794 }
1795 p.lineTo(endPos);
1796
1797 decor.insert(handle, p);
1798}
1799
1800void processEdges(QTransform t, QMap<SvgTextCursor::TypeSettingModeHandle, int> values,
1801 const bool isHorizontal,
1802 const qreal scaleMetrics,
1803 const QPointF advance,
1804 QPainterPath &path) {
1805 QPointF p1(values.first(), 0);
1806 QPointF p2(values.last(), 0);
1807 Q_FOREACH(const int val, values) {
1808 if (val < p1.x()) {
1809 p1.setX(val);
1810 }
1811 if (val > p2.x()) {
1812 p2.setX(val);
1813 }
1814 }
1815 if (isHorizontal) {
1816 p1 = QPointF(p1.y(), -p1.x());
1817 p2 = QPointF(p2.y(), -p2.x());
1818 }
1819 p1 *= scaleMetrics;
1820 p2 *= scaleMetrics;
1821 path.moveTo(t.map(p1+advance));
1822 path.lineTo(t.map(p2+advance));
1823}
1824
1825QTransform posAndRotateTransform(const QPointF pos, const qreal rotateDeg) {
1826 QTransform t = QTransform::fromTranslate(pos.x(), pos.y());
1827 t.rotate(rotateDeg);
1828 return t;
1829}
1830
1832{
1833 QRectF updateRect;
1834 if (d->shape && d->typeSettingMode) {
1835
1837 d->shape->getPositionsAndRotationsForRange(d->pos, d->anchor);
1838 if (infos.size() < 1) return;
1839
1840 const bool rtl = infos.first().rtl;
1841
1842 KoSvgTextCharacterInfo first = infos.first();
1843 KoSvgTextCharacterInfo last = infos.last();
1844 if (infos.size() > 1) {
1845 std::sort(infos.begin(), infos.end(), KoSvgTextCharacterInfo::visualLessThan);
1846 for (auto it = infos.begin(); it != infos.end(); it++) {
1847 if (it->visualIndex >= 0) {
1848 first = *it;
1849 break;
1850 }
1851 }
1852 for (auto it = infos.rbegin(); it != infos.rend(); it++) {
1853 if (it->visualIndex >= 0) {
1854 last = *it;
1855 break;
1856 }
1857 }
1858 }
1859
1860 QTransform t = QTransform::fromTranslate(last.finalPos.x(), last.finalPos.y());
1861 t.rotate(last.rotateDeg);
1862 last.finalPos = t.map(last.advance);
1863
1864 d->typeSettingDecor.handles.first = rtl? last.finalPos: first.finalPos;
1865 d->typeSettingDecor.handles.second = rtl? first.finalPos: last.finalPos;
1866
1867 //Start collecting the metrics decoration...
1868 d->typeSettingDecor.paths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1869 d->typeSettingDecor.baselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1870 d->typeSettingDecor.edges = QPainterPath();
1871 d->typeSettingDecor.parentPaths = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1872 d->typeSettingDecor.parentBaselines = QMap<SvgTextCursor::TypeSettingModeHandle, QPainterPath>();
1873 QList<KoSvgTextCharacterInfo> metricInfos = infos;
1874 if (d->pos == d->anchor) {
1875 metricInfos = d->shape->getPositionsAndRotationsForRange(0, d->shape->posForIndex(d->shape->plainText().size()));
1876 }
1877 const bool isHorizontal = d->shape->writingMode() == KoSvgText::HorizontalTB;
1878 const int minPos = qMin(d->pos, d->anchor);
1879 const int maxPos = qMax(d->pos, d->anchor);
1880 const int endPos = d->shape->posForIndex(d->shape->plainText().size());
1881
1883 if (minPos > 0 || maxPos < endPos) {
1884 const KoSvgTextProperties props = d->shape->textProperties();
1886 KoSvgText::FontMetrics metrics = props.metrics(true, true);
1887 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1888 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1889
1890 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1891 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1892
1894 QList<int> positions;
1895 positions << 0;
1896 positions << qMax(0, minPos-1);
1897 positions << maxPos;
1898 positions << endPos;
1899
1900 Q_FOREACH(const int pos, positions) {
1902 d->shape->getPositionsAndRotationsForRange(pos, pos);
1903 parentInfos.append(info.first());
1904 }
1905
1906 QPointF drawOffset = isHorizontal? QPointF(d->handleRadius*2, 0): QPointF(0, d->handleRadius*2);
1907 if (d->canvas) {
1908 drawOffset = d->canvas->viewConverter()->viewToDocument().map(drawOffset);
1909 }
1910 bool toggleOffset = rtl;
1911
1912 for (auto it = parentInfos.begin(); it != parentInfos.end(); it++) {
1913 const bool currentIsMin = (d->shape->posForIndex(it->logicalIndex) == minPos);
1914 const QPointF finalPos = toggleOffset? it->finalPos + (it->advance - drawOffset): it->finalPos;
1915 const QPointF advance = drawOffset;
1916
1917 const QTransform t = posAndRotateTransform(finalPos, it->rotateDeg);
1918
1919 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1920 const int metric = types.value(handle);
1921 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, advance, d->typeSettingDecor.parentBaselines);
1922 }
1923
1924 {
1925
1926 if (currentIsMin && minPos == maxPos && !toggleOffset) {
1927 toggleOffset = !toggleOffset;
1928 continue;
1929 }
1930 const QPointF advance = toggleOffset? it->advance: QPointF();
1931 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1932 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1933 }
1934
1935 toggleOffset = !toggleOffset;
1936 }
1937 }
1938
1940 bool toggleOffset = false; // MetricInfos are visually-ordered.
1941 for (auto it = metricInfos.begin(); it != metricInfos.end(); it++) {
1942 const int currentPos = (d->pos == d->anchor)? -1 :d->shape->posForIndex(it->logicalIndex);
1943
1944 KoSvgTextProperties props = d->shape->propertiesForPos(currentPos, true);
1946 KoSvgText::FontMetrics propMetrics = props.metrics(true, true);
1947
1948 KoSvgText::FontMetrics metrics = (d->pos == d->anchor)? propMetrics :it->metrics;
1949
1950 const qreal scaleMetrics = props.fontSize().value/qreal(metrics.fontSize);
1951 const int lineGap = calcLineHeight(lineHeight, metrics, scaleMetrics);
1952
1953 const QTransform t = posAndRotateTransform(it->finalPos, it->rotateDeg);
1954
1955 const QMap<SvgTextCursor::TypeSettingModeHandle, int> types
1956 = typeSettingBaselinesFromMetrics(metrics, lineGap, isHorizontal);
1957
1958 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, types.keys()) {
1959 const int metric = types.value(handle);
1960 processBaseline(handle, metric, isHorizontal, t, scaleMetrics, it->advance, d->typeSettingDecor.baselines);
1961 }
1962 if ((currentPos == minPos || currentPos+1 == maxPos) && minPos != maxPos) {
1963 const QPointF advance = toggleOffset? it->advance: QPointF();
1964 toggleOffset = !toggleOffset;
1965 processEdges(t, types, isHorizontal, scaleMetrics, advance, d->typeSettingDecor.edges);
1966 }
1967 }
1968
1970 const QList<SvgTextCursor::TypeSettingModeHandle> nonBaselines = {
1972 };
1973 Q_FOREACH(SvgTextCursor::TypeSettingModeHandle handle, nonBaselines) {
1974 d->typeSettingDecor.paths.insert(handle, d->typeSettingDecor.baselines.value(handle));
1975 d->typeSettingDecor.parentPaths.insert(handle, d->typeSettingDecor.parentBaselines.value(handle));
1976 }
1977 d->typeSettingDecor.baselines.remove(LineHeightTop);
1978 d->typeSettingDecor.baselines.remove(LineHeightBottom);
1979 d->typeSettingDecor.parentBaselines.remove(LineHeightTop);
1980 d->typeSettingDecor.parentBaselines.remove(LineHeightBottom);
1981
1982 updateRect = d->shape->shapeToDocument(d->typeSettingDecor.boundingRect(d->handleRadius));
1983 }
1984 updateTypeSettingDecorFromShape(); // To remove the text-in-path nodes..
1985 Q_EMIT updateCursorDecoration(updateRect | d->oldTypeSettingRect);
1986 d->oldTypeSettingRect = updateRect;
1987}
1988
1990{
1991 if (d->canvas) {
1992 if (cmd) {
1993 d->isAddingCommand = true;
1994 d->canvas->addCommand(cmd);
1995 d->isAddingCommand = false;
1996 }
1997 }
1998}
1999
2000int SvgTextCursor::moveModeResult(const SvgTextCursor::MoveMode mode, int &pos, bool visual) const
2001{
2002 int newPos = pos;
2003 switch (mode) {
2004 case MoveNone:
2005 break;
2006 case MoveLeft:
2007 newPos = d->shape->posLeft(pos, visual);
2008 break;
2009 case MoveRight:
2010 newPos = d->shape->posRight(pos, visual);
2011 break;
2012 case MoveUp:
2013 newPos = d->shape->posUp(pos, visual);
2014 break;
2015 case MoveDown:
2016 newPos = d->shape->posDown(pos, visual);
2017 break;
2018 case MovePreviousChar:
2019 newPos = d->shape->previousIndex(pos);
2020 break;
2021 case MoveNextChar:
2022 newPos = d->shape->nextIndex(pos);
2023 break;
2024 case MovePreviousLine:
2025 newPos = d->shape->previousLine(pos);
2026 break;
2027 case MoveNextLine:
2028 newPos = d->shape->nextLine(pos);
2029 break;
2030 case MoveWordLeft:
2031 newPos = d->shape->wordLeft(pos, visual);
2032 if (newPos == pos) {
2033 newPos = d->shape->posLeft(pos, visual);
2034 newPos = d->shape->wordLeft(newPos, visual);
2035 }
2036 break;
2037 case MoveWordRight:
2038 newPos = d->shape->wordRight(pos, visual);
2039 if (newPos == pos) {
2040 newPos = d->shape->posRight(pos, visual);
2041 newPos = d->shape->wordRight(newPos, visual);
2042 }
2043 break;
2044 case MoveWordStart:
2045 newPos = d->shape->wordStart(pos);
2046 if (newPos == pos) {
2047 newPos = d->shape->previousIndex(pos);
2048 newPos = d->shape->wordStart(newPos);
2049 }
2050 break;
2051 case MoveWordEnd:
2052 newPos = d->shape->wordEnd(pos);
2053 if (newPos == pos) {
2054 newPos = d->shape->nextIndex(pos);
2055 newPos = d->shape->wordEnd(newPos);
2056 }
2057 break;
2058 case MoveLineStart:
2059 newPos = d->shape->lineStart(pos);
2060 break;
2061 case MoveLineEnd:
2062 newPos = d->shape->lineEnd(pos);
2063 break;
2064 case ParagraphStart:
2065 newPos = 0;
2066 break;
2067 case ParagraphEnd:
2068 newPos = d->shape->posForIndex(d->shape->plainText().size());
2069 break;
2070 }
2071 return newPos;
2072}
2073
2075bool SvgTextCursor::acceptableInput(const QKeyEvent *event) const
2076{
2077 const QString text = event->text();
2078 if (text.isEmpty())
2079 return false;
2080 const QChar c = text.at(0);
2081 // Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the
2082 // next test, since CTRL+SHIFT is sometimes used to input it on Windows.
2083 if (c.category() == QChar::Other_Format)
2084 return true;
2085 // QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards
2086 if (event->modifiers() == Qt::ControlModifier
2087 || event->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
2088 return false;
2089 }
2090 if (c.isPrint())
2091 return true;
2092 if (c.category() == QChar::Other_PrivateUse)
2093 return true;
2094 if (c == QLatin1Char('\t'))
2095 return true;
2096 return false;
2097}
2098
2100{
2101 if (!d->preEditCommand) {
2102 return;
2103 }
2104
2105 qApp->inputMethod()->commit();
2106
2107 if (!d->preEditCommand) {
2108 return;
2109 }
2110
2111 d->preEditCommand->undo();
2112 d->preEditCommand = nullptr;
2113 d->preEditStart = -1;
2114 d->preEditLength = 0;
2116 updateCursor();
2117}
2118
2120{
2121 // Only update canvas resources when there's no selection.
2122 // This relies on Krita not setting anything on the text when there's no selection.
2123 if (d->shape && d->canvas->resourceManager() && d->pos == d->anchor) {
2124 KoSvgTextProperties props = hasSelection()? d->shape->propertiesForPos(qMin(d->pos, d->anchor), true): d->shape->textProperties();
2125 KoColorBackground *bg = dynamic_cast<KoColorBackground *>(props.background().data());
2126 if (bg && props.hasProperty(KoSvgTextProperties::FillId)) {
2127 KoColor c;
2128 c.fromQColor(bg->color());
2129 c.setOpacity(1.0);
2130 if (c != d->canvas->resourceManager()->foregroundColor()) {
2131 d->canvas->resourceManager()->setForegroundColor(c);
2132 }
2133 }
2134 KoShapeStroke *stroke = dynamic_cast<KoShapeStroke *>(props.stroke().data());
2135 if (stroke && stroke->isVisible() && stroke->color().isValid() && props.hasProperty(KoSvgTextProperties::StrokeId)) {
2136 KoColor c;
2137 c.fromQColor(stroke->color());
2138 c.setOpacity(1.0);
2139 if (c != d->canvas->resourceManager()->backgroundColor()) {
2140 d->canvas->resourceManager()->setBackgroundColor(c);
2141 }
2142 }
2143
2144 Q_FOREACH (QAction *action, d->actions) {
2145 // Blocking signals so that we don't get a toggle action while evaluating the checked-ness.
2146 action->blockSignals(true);
2147 const QList<KoSvgTextProperties> r = d->shape->propertiesForRange(qMin(d->pos, d->anchor), qMax(d->pos, d->anchor), true);
2148 if (action->isCheckable() && SvgTextShortCuts::possibleActions().contains(action->objectName())) {
2149 const bool checked = SvgTextShortCuts::actionEnabled(action, r);
2150 if (action->isChecked() != checked) {
2151 action->setChecked(checked);
2152 }
2153 }
2154 action->blockSignals(false);
2155 }
2156 }
2157}
2158
2168
2170 : KoSvgTextPropertiesInterface(parent), d(new Private(parent))
2171{
2172 connect(&d->compressor, SIGNAL(timeout()), this, SIGNAL(textSelectionChanged()));
2173 connect(&d->characterCompressor, SIGNAL(timeout()), this, SIGNAL(textCharacterSelectionChanged()));
2174}
2175
2181{
2182 return d->parent->propertiesForShape();
2183}
2184
2186{
2187 // When there's only a single node, its best to only return empty properties, as paragraph properties handle that single node.
2188 if (!d->parent->shape()) return QList<KoSvgTextProperties>();
2189 if (d->parent->shape()->singleNode()) {
2190 return {KoSvgTextProperties()};
2191 }
2192 return d->parent->propertiesForRange();
2193}
2194
2196{
2197 // 9 times out of 10 this is correct, though we could do better by actually
2198 // getting inherited properties for the range and not just defaulting to the paragraph.
2199 return (d->parent->shape())? d->parent->shape()->textProperties(): KoSvgTextProperties();
2200}
2201
2202void SvgTextCursorPropertyInterface::setPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2203{
2204 d->parent->mergePropertiesIntoSelection(properties, removeProperties, true);
2205}
2206
2207void SvgTextCursorPropertyInterface::setCharacterPropertiesOnSelected(KoSvgTextProperties properties, QSet<KoSvgTextProperties::PropertyId> removeProperties)
2208{
2209 d->parent->mergePropertiesIntoSelection(properties, removeProperties, false, true);
2210}
2211
2213{
2214 return d->parent->hasSelection();
2215}
2216
2221
2223{
2224 // Don't bother updating the selection when there's no shape
2225 // this is so we can use the text properties last used to create new texts.
2226 if (!d->parent->shape()) return;
2227 d->compressor.start();
2228}
2229
2231{
2232 d->characterCompressor.start();
2233}
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)