File indexing completed on 2024-12-22 04:16:45

0001 /* This file is part of the KDE project
0002 
0003    SPDX-FileCopyrightText: 2017 Boudewijn Rempt <boud@valdyas.org>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "SvgTextTool.h"
0009 #include "KoSvgTextProperties.h"
0010 #include "KoSvgTextShape.h"
0011 #include "KoSvgTextShapeMarkupConverter.h"
0012 #include "SvgCreateTextStrategy.h"
0013 #include "SvgInlineSizeChangeCommand.h"
0014 #include "SvgInlineSizeChangeStrategy.h"
0015 #include "SvgSelectTextStrategy.h"
0016 #include "SvgInlineSizeHelper.h"
0017 #include "SvgMoveTextCommand.h"
0018 #include "SvgMoveTextStrategy.h"
0019 #include "SvgTextChangeCommand.h"
0020 #include "SvgTextEditor.h"
0021 #include "SvgTextRemoveCommand.h"
0022 
0023 #include <QLabel>
0024 #include <QPainterPath>
0025 #include <QToolButton>
0026 #include <QGridLayout>
0027 #include <QVBoxLayout>
0028 #include <QDesktopServices>
0029 #include <QApplication>
0030 #include <QGroupBox>
0031 #include <QFontDatabase>
0032 #include <QButtonGroup>
0033 #include <QMenuBar>
0034 
0035 #include <klocalizedstring.h>
0036 
0037 #include <KisPart.h>
0038 #include <kis_canvas2.h>
0039 #include <KSharedConfig>
0040 #include "kis_assert.h"
0041 #include <kis_coordinates_converter.h>
0042 
0043 #include <KoFileDialog.h>
0044 #include <KoIcon.h>
0045 #include <KoCanvasBase.h>
0046 #include <KoSelection.h>
0047 #include <KoShapeManager.h>
0048 #include <KoShapeController.h>
0049 #include <KoShapeRegistry.h>
0050 #include <KoShapeFactoryBase.h>
0051 #include <KoPointerEvent.h>
0052 #include <KoProperties.h>
0053 #include <KoSelectedShapesProxy.h>
0054 #include "KoToolManager.h"
0055 #include <KoShapeFillWrapper.h>
0056 #include "KoCanvasResourceProvider.h"
0057 #include <KoPathShape.h>
0058 #include <KoPathSegment.h>
0059 
0060 #include "KisHandlePainterHelper.h"
0061 #include "kis_tool_utils.h"
0062 #include <commands/KoKeepShapesSelectedCommand.h>
0063 
0064 
0065 using SvgInlineSizeHelper::InlineSizeInfo;
0066 
0067 constexpr double INLINE_SIZE_DASHES_PATTERN_A = 4.0; /// Size of the visible part of the inline-size handle dashes.
0068 constexpr double INLINE_SIZE_DASHES_PATTERN_B = 8.0; /// Size of the hidden part of the inline-size handle dashes.
0069 constexpr int INLINE_SIZE_DASHES_PATTERN_LENGTH = 3; /// Total amount of trailing dashes on inline-size handles.
0070 constexpr double INLINE_SIZE_HANDLE_THICKNESS = 1.0; /// Linethickness.
0071 
0072 
0073 static bool debugEnabled()
0074 {
0075     static const bool debugEnabled = !qEnvironmentVariableIsEmpty("KRITA_DEBUG_TEXTTOOL");
0076     return debugEnabled;
0077 }
0078 
0079 SvgTextTool::SvgTextTool(KoCanvasBase *canvas)
0080     : KoToolBase(canvas)
0081     , m_textCursor(canvas)
0082 {
0083      // TODO: figure out whether we should use system config for this, Windows and GTK have values for it, but Qt and MacOS don't(?).
0084     int cursorFlashLimit = 5000;
0085     m_textCursor.setCaretSetting(QApplication::style()->pixelMetric(QStyle::PM_TextCursorWidth)
0086                                  , qApp->cursorFlashTime()
0087                                  , cursorFlashLimit);
0088     connect(&m_textCursor, SIGNAL(updateCursorDecoration(QRectF)), this, SLOT(slotUpdateCursorDecoration(QRectF)));
0089 
0090     m_base_cursor = QCursor(QPixmap(":/tool_text_basic.xpm"), 7, 7);
0091     m_text_inline_horizontal = QCursor(QPixmap(":/tool_text_inline_horizontal.xpm"), 7, 7);
0092     m_text_inline_vertical = QCursor(QPixmap(":/tool_text_inline_vertical.xpm"), 7, 7);
0093     m_text_on_path = QCursor(QPixmap(":/tool_text_on_path.xpm"), 7, 7);
0094     m_text_in_shape = QCursor(QPixmap(":/tool_text_in_shape.xpm"), 7, 7);
0095     m_ibeam_horizontal = QCursor(QPixmap(":/tool_text_i_beam_horizontal.xpm"), 11, 11);
0096     m_ibeam_vertical = QCursor(QPixmap(":/tool_text_i_beam_vertical.xpm"), 11, 11);
0097     m_ibeam_horizontal_done = QCursor(QPixmap(":/tool_text_i_beam_horizontal_done.xpm"), 5, 11);
0098 }
0099 
0100 SvgTextTool::~SvgTextTool()
0101 {
0102     if(m_editor) {
0103         m_editor->close();
0104     }
0105     delete m_defAlignment;
0106 }
0107 
0108 void SvgTextTool::activate(const QSet<KoShape *> &shapes)
0109 {
0110     KoToolBase::activate(shapes);
0111     m_canvasConnections.addConnection(canvas()->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(slotShapeSelectionChanged()));
0112 
0113     useCursor(m_base_cursor);
0114     slotShapeSelectionChanged();
0115 
0116     repaintDecorations();
0117 }
0118 
0119 void SvgTextTool::deactivate()
0120 {
0121     KoToolBase::deactivate();
0122     m_canvasConnections.clear();
0123 
0124     m_hoveredShapeHighlightRect = QPainterPath();
0125 
0126     repaintDecorations();
0127 }
0128 
0129 KisPopupWidgetInterface *SvgTextTool::popupWidget()
0130 {
0131     return nullptr;
0132 }
0133 
0134 QVariant SvgTextTool::inputMethodQuery(Qt::InputMethodQuery query) const
0135 {
0136     if (canvas()) {
0137         return m_textCursor.inputMethodQuery(query);
0138     } else {
0139         return KoToolBase::inputMethodQuery(query);
0140     }
0141 }
0142 
0143 void SvgTextTool::inputMethodEvent(QInputMethodEvent *event)
0144 {
0145     m_textCursor.inputMethodEvent(event);
0146 }
0147 
0148 QWidget *SvgTextTool::createOptionWidget()
0149 {
0150     QWidget *optionWidget = new QWidget();
0151     optionUi.setupUi(optionWidget);
0152 
0153     if (!debugEnabled()) {
0154         optionUi.groupBoxDebug->hide();
0155     }
0156 
0157     m_configGroup = KSharedConfig::openConfig()->group(toolId());
0158 
0159     QString storedFont = m_configGroup.readEntry<QString>("defaultFont", QApplication::font().family());
0160     optionUi.defFont->setCurrentFont(QFont(storedFont));
0161     Q_FOREACH (int size, QFontDatabase::standardSizes()) {
0162         optionUi.defPointSize->addItem(QString::number(size)+" pt");
0163     }
0164     int storedSize = m_configGroup.readEntry<int>("defaultSize", QApplication::font().pointSize());
0165 #ifdef Q_OS_ANDROID
0166     // HACK: on some devices where android.R.styleable exists, Qt's platform
0167     // plugin sets the pixelSize of a font, which returns -1 when asked for pointSize.
0168     //
0169     // The way to fetch font in Qt from SDK is deprecated in newer Android versions.
0170     if (storedSize <= 0) {
0171         storedSize = 18;  // being one of the standardSizes
0172     }
0173 #endif
0174     int sizeIndex = 0;
0175     if (QFontDatabase::standardSizes().contains(storedSize)) {
0176         sizeIndex = QFontDatabase::standardSizes().indexOf(storedSize);
0177     }
0178     optionUi.defPointSize->setCurrentIndex(sizeIndex);
0179 
0180     int checkedAlignment = m_configGroup.readEntry<int>("defaultAlignment", 0);
0181 
0182     m_defAlignment = new QButtonGroup();
0183     optionUi.alignLeft->setIcon(KisIconUtils::loadIcon("format-justify-left"));
0184     m_defAlignment->addButton(optionUi.alignLeft, 0);
0185 
0186     optionUi.alignCenter->setIcon(KisIconUtils::loadIcon("format-justify-center"));
0187     m_defAlignment->addButton(optionUi.alignCenter, 1);
0188 
0189     optionUi.alignRight->setIcon(KisIconUtils::loadIcon("format-justify-right"));
0190     m_defAlignment->addButton(optionUi.alignRight, 2);
0191 
0192     m_defAlignment->setExclusive(true);
0193     if (checkedAlignment<1) {
0194         optionUi.alignLeft->setChecked(true);
0195     } else if (checkedAlignment==1) {
0196         optionUi.alignCenter->setChecked(true);
0197     } else if (checkedAlignment==2) {
0198         optionUi.alignRight->setChecked(true);
0199     } else {
0200         optionUi.alignLeft->setChecked(true);
0201     }
0202 
0203     int checkedWritingMode = m_configGroup.readEntry<int>("defaultWritingMode", 0);
0204 
0205     m_defWritingMode = new QButtonGroup();
0206     optionUi.modeHorizontalTb->setIcon(KisIconUtils::loadIcon("format-text-direction-horizontal-tb"));
0207     m_defWritingMode->addButton(optionUi.modeHorizontalTb, 0);
0208 
0209     optionUi.modeVerticalRl->setIcon(KisIconUtils::loadIcon("format-text-direction-vertical-rl"));
0210     m_defWritingMode->addButton(optionUi.modeVerticalRl, 1);
0211 
0212     optionUi.modeVerticalLr->setIcon(KisIconUtils::loadIcon("format-text-direction-vertical-lr"));
0213     m_defWritingMode->addButton(optionUi.modeVerticalLr, 2);
0214 
0215     m_defWritingMode->setExclusive(true);
0216     if (checkedWritingMode<1) {
0217         optionUi.modeHorizontalTb->setChecked(true);
0218     } else if (checkedWritingMode==1) {
0219         optionUi.modeVerticalRl->setChecked(true);
0220     } else if (checkedWritingMode==2) {
0221         optionUi.modeVerticalLr->setChecked(true);
0222     } else {
0223         optionUi.modeHorizontalTb->setChecked(true);
0224     }
0225 
0226     bool rtl = m_configGroup.readEntry<int>("defaultWritingDirection", false);
0227     m_defDirection = new QButtonGroup();
0228 
0229     optionUi.directionLtr->setIcon(KisIconUtils::loadIcon("format-text-direction-ltr"));
0230     m_defDirection->addButton(optionUi.directionLtr, 0);
0231 
0232     optionUi.directionRtl->setIcon(KisIconUtils::loadIcon("format-text-direction-rtl"));
0233     m_defDirection->addButton(optionUi.directionRtl, 1);
0234     m_defDirection->setExclusive(true);
0235     optionUi.directionLtr->setChecked(!rtl);
0236     optionUi.directionRtl->setChecked(rtl);
0237 
0238     bool directionEnabled = !bool(m_defWritingMode->checkedId());
0239     optionUi.directionLtr->setEnabled(directionEnabled);
0240     optionUi.directionRtl->setEnabled(directionEnabled);
0241 
0242     double storedLetterSpacing = m_configGroup.readEntry<double>("defaultLetterSpacing", 0.0);
0243     optionUi.defLetterSpacing->setValue(storedLetterSpacing);
0244 
0245     connect(m_defAlignment, SIGNAL(buttonClicked(int)), this, SLOT(storeDefaults()));
0246     connect(m_defWritingMode, SIGNAL(buttonClicked(int)), this, SLOT(storeDefaults()));
0247     connect(m_defDirection, SIGNAL(buttonClicked(int)), this, SLOT(storeDefaults()));
0248 
0249     connect(optionUi.defFont, SIGNAL(currentFontChanged(QFont)), this, SLOT(storeDefaults()));
0250     connect(optionUi.defPointSize, SIGNAL(currentIndexChanged(int)), this, SLOT(storeDefaults()));
0251     connect(optionUi.defLetterSpacing, SIGNAL(valueChanged(double)), SLOT(storeDefaults()));
0252 
0253     connect(optionUi.btnEdit, SIGNAL(clicked(bool)), SLOT(showEditor()));
0254     connect(optionUi.btnEditSvg, SIGNAL(clicked(bool)), SLOT(showEditorSvgSource()));
0255 
0256     return optionWidget;
0257 }
0258 
0259 KoSelection *SvgTextTool::koSelection() const
0260 {
0261     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas(), 0);
0262     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas()->selectedShapesProxy(), 0);
0263 
0264     return canvas()->selectedShapesProxy()->selection();
0265 }
0266 
0267 KoSvgTextShape *SvgTextTool::selectedShape() const
0268 {
0269     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas(), 0);
0270     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas()->selectedShapesProxy(), 0);
0271 
0272     QList<KoShape*> shapes = koSelection()->selectedEditableShapes();
0273     if (shapes.isEmpty()) return 0;
0274 
0275     KoSvgTextShape *textShape = dynamic_cast<KoSvgTextShape*>(shapes.first());
0276 
0277     return textShape;
0278 }
0279 
0280 void SvgTextTool::showEditor()
0281 {
0282     KoSvgTextShape *shape = selectedShape();
0283     if (!shape) return;
0284 
0285     if (!m_editor) {
0286         m_editor = new SvgTextEditor(QApplication::activeWindow());
0287         m_editor->setWindowTitle(i18nc("@title:window", "Krita - Edit Text"));
0288         m_editor->setWindowModality(Qt::ApplicationModal);
0289         m_editor->setAttribute( Qt::WA_QuitOnClose, false );
0290 
0291         connect(m_editor, SIGNAL(textUpdated(KoSvgTextShape*,QString,QString)), SLOT(textUpdated(KoSvgTextShape*,QString,QString)));
0292         connect(m_editor, SIGNAL(textEditorClosed()), SLOT(slotTextEditorClosed()));
0293 
0294         m_editor->activateWindow(); // raise on creation only
0295     }
0296     if (!m_editor->isVisible()) {
0297         m_editor->setInitialShape(shape);
0298 #ifdef Q_OS_ANDROID
0299         // for window manager
0300         m_editor->setWindowFlags(Qt::Dialog);
0301         m_editor->menuBar()->setNativeMenuBar(false);
0302 #endif
0303         m_editor->show();
0304     }
0305 }
0306 
0307 void SvgTextTool::showEditorSvgSource()
0308 {
0309     KoSvgTextShape *shape = selectedShape();
0310     if (!shape) {
0311         return;
0312     }
0313     showEditor();
0314 }
0315 
0316 void SvgTextTool::textUpdated(KoSvgTextShape *shape, const QString &svg, const QString &defs)
0317 {
0318     SvgTextChangeCommand *cmd = new SvgTextChangeCommand(shape, svg, defs);
0319     canvas()->addCommand(cmd);
0320 }
0321 
0322 void SvgTextTool::slotTextEditorClosed()
0323 {
0324     // change tools to the shape selection tool when we close the text editor to allow moving and further editing of the object.
0325     // most of the time when we edit text, the shape selection tool is where we left off anyway
0326     KoToolManager::instance()->switchToolRequested("InteractionTool");
0327 }
0328 
0329 QString SvgTextTool::generateDefs(const QString &extraProperties)
0330 {
0331     QString font = optionUi.defFont->currentFont().family();
0332     QString size = QString::number(QFontDatabase::standardSizes().at(optionUi.defPointSize->currentIndex() > -1 ? optionUi.defPointSize->currentIndex() : 0));
0333 
0334     QString textAnchor = "middle";
0335     if (m_defAlignment->button(0)->isChecked()) {
0336         textAnchor = "start";
0337     }
0338     if (m_defAlignment->button(2)->isChecked()) {
0339         textAnchor = "end";
0340     }
0341 
0342     QString direction = "ltr";
0343     QString writingMode = "horizontal-tb";
0344     QString textOrientation = "text-orientation: upright";
0345     if (m_defWritingMode->button(1)->isChecked()) {
0346         writingMode = "vertical-rl;" + textOrientation;
0347     } else if (m_defWritingMode->button(2)->isChecked()) {
0348         writingMode = "vertical-lr;" + textOrientation;
0349     } else {
0350         direction = m_defDirection->button(0)->isChecked()? "ltr" : "rtl";
0351     }
0352 
0353     QString fontColor = canvas()->resourceManager()->foregroundColor().toQColor().name();
0354     QString letterSpacing = QString::number(optionUi.defLetterSpacing->value());
0355 
0356     return QString("<defs>\n <style>\n  text {\n   font-family:'%1';\n   font-size:%2 ; fill:%3 ;  text-anchor:%4; letter-spacing:%5; writing-mode:%6; direction: %7; %8\n  white-space:pre-wrap;\n  }\n </style>\n</defs>")
0357             .arg(font, size, fontColor, textAnchor, letterSpacing, writingMode, direction, extraProperties);
0358 }
0359 
0360 void SvgTextTool::storeDefaults()
0361 {
0362     m_configGroup = KSharedConfig::openConfig()->group(toolId());
0363     m_configGroup.writeEntry("defaultFont", optionUi.defFont->currentFont().family());
0364     m_configGroup.writeEntry("defaultSize", QFontDatabase::standardSizes().at(optionUi.defPointSize->currentIndex() > -1 ? optionUi.defPointSize->currentIndex() : 0));
0365     m_configGroup.writeEntry("defaultAlignment", m_defAlignment->checkedId());
0366     m_configGroup.writeEntry("defaultLetterSpacing", optionUi.defLetterSpacing->value());
0367     m_configGroup.writeEntry("defaultWritingMode", m_defWritingMode->checkedId());
0368     m_configGroup.writeEntry("defaultWritingDirection", m_defDirection->checkedId());
0369 
0370     bool directionEnabled = !bool(m_defWritingMode->checkedId());
0371     optionUi.directionLtr->setEnabled(directionEnabled);
0372     optionUi.directionRtl->setEnabled(directionEnabled);
0373 }
0374 
0375 void SvgTextTool::slotShapeSelectionChanged()
0376 {
0377     QList<KoShape *> shapes = koSelection()->selectedShapes();
0378     if (shapes.size() == 1) {
0379         KoSvgTextShape *textShape = dynamic_cast<KoSvgTextShape*>(*shapes.constBegin());
0380         if (!textShape) {
0381             koSelection()->deselectAll();
0382             return;
0383         }
0384     } else if (shapes.size() > 1) {
0385         KoSvgTextShape *foundTextShape = nullptr;
0386 
0387         Q_FOREACH (KoShape *shape, shapes) {
0388             KoSvgTextShape *textShape = dynamic_cast<KoSvgTextShape*>(shape);
0389             if (textShape) {
0390                 foundTextShape = textShape;
0391                 break;
0392             }
0393         }
0394 
0395         koSelection()->deselectAll();
0396         if (foundTextShape) {
0397             koSelection()->select(foundTextShape);
0398         }
0399         return;
0400     }
0401     KoSvgTextShape *const shape = selectedShape();
0402     if (shape != m_textCursor.shape()) {
0403         m_textCursor.setShape(shape);
0404         if (shape) {
0405             setTextMode(true);
0406         } else {
0407             setTextMode(false);
0408         }
0409     }
0410 }
0411 
0412 void SvgTextTool::copy() const
0413 {
0414     m_textCursor.copy();
0415 }
0416 
0417 void SvgTextTool::deleteSelection()
0418 {
0419     m_textCursor.removeSelection();
0420 }
0421 
0422 bool SvgTextTool::paste()
0423 {
0424     return m_textCursor.paste();
0425 }
0426 
0427 bool SvgTextTool::hasSelection()
0428 {
0429     return m_textCursor.hasSelection();
0430 }
0431 
0432 bool SvgTextTool::selectAll()
0433 {
0434     m_textCursor.moveCursor(SvgTextCursor::ParagraphStart, true);
0435     m_textCursor.moveCursor(SvgTextCursor::ParagraphEnd, false);
0436     return true;
0437 }
0438 
0439 void SvgTextTool::deselect()
0440 {
0441     m_textCursor.deselectText();
0442 }
0443 
0444 KoToolSelection *SvgTextTool::selection()
0445 {
0446     return &m_textCursor;
0447 }
0448 
0449 void SvgTextTool::requestStrokeEnd()
0450 {
0451     if (!m_textCursor.isAddingCommand() && !m_strategyAddingCommand) {
0452         if (m_interactionStrategy) {
0453             m_dragging = DragMode::None;
0454             m_interactionStrategy->cancelInteraction();
0455             m_interactionStrategy = nullptr;
0456             useCursor(Qt::ArrowCursor);
0457         } else if (isInTextMode()) {
0458             canvas()->shapeManager()->selection()->deselectAll();
0459         }
0460     }
0461 }
0462 
0463 void SvgTextTool::requestStrokeCancellation()
0464 {
0465     /**
0466      * Doing nothing, since these signals come on undo/redo actions
0467      * in the mainland undo stack, which we manipulate while editing
0468      * text
0469      */
0470 }
0471 
0472 void SvgTextTool::slotUpdateCursorDecoration(QRectF updateRect)
0473 {
0474     if (canvas()) {
0475         canvas()->updateCanvas(updateRect);
0476     }
0477 }
0478 
0479 QFont SvgTextTool::defaultFont() const
0480 {
0481     int size = QFontDatabase::standardSizes().at(optionUi.defPointSize->currentIndex() > -1 ? optionUi.defPointSize->currentIndex() : 0);
0482     QFont font = optionUi.defFont->currentFont();
0483     font.setPointSize(size);
0484     return font;
0485 }
0486 
0487 Qt::Alignment SvgTextTool::horizontalAlign() const
0488 {
0489     if (m_defAlignment->button(1)->isChecked()) {
0490         return Qt::AlignHCenter;
0491     }
0492     if (m_defAlignment->button(2)->isChecked()) {
0493         return Qt::AlignRight;
0494     }
0495     return Qt::AlignLeft;
0496 }
0497 
0498 int SvgTextTool::writingMode() const
0499 {
0500     return m_defWritingMode->checkedId();
0501 }
0502 
0503 bool SvgTextTool::isRtl() const
0504 {
0505     return m_defDirection->checkedId();
0506 }
0507 
0508 QRectF SvgTextTool::decorationsRect() const
0509 {
0510     QRectF rect;
0511     KoSvgTextShape *const shape = selectedShape();
0512     if (shape) {
0513         rect |= shape->boundingRect();
0514 
0515         const QPointF anchor = shape->absoluteTransformation().map(QPointF());
0516         rect |= kisGrowRect(QRectF(anchor, anchor), handleRadius());
0517 
0518         qreal pxlToPt = canvas()->viewConverter()->viewToDocumentX(1.0);
0519         qreal length = (INLINE_SIZE_DASHES_PATTERN_A + INLINE_SIZE_DASHES_PATTERN_B) * INLINE_SIZE_DASHES_PATTERN_LENGTH;
0520 
0521         if (std::optional<InlineSizeInfo> info = InlineSizeInfo::fromShape(shape, length * pxlToPt)) {
0522             rect |= kisGrowRect(info->boundingRect(), handleRadius() * 2);
0523         }
0524 
0525         if (canvas()->snapGuide()->isSnapping()) {
0526             rect |= canvas()->snapGuide()->boundingRect();
0527         }
0528     }
0529 
0530     rect |= m_hoveredShapeHighlightRect.boundingRect();
0531 
0532     return rect;
0533 }
0534 
0535 void SvgTextTool::paint(QPainter &gc, const KoViewConverter &converter)
0536 {
0537     if (!isActivated()) return;
0538 
0539     if (m_dragging == DragMode::Create) {
0540         m_interactionStrategy->paint(gc, converter);
0541     }
0542 
0543     KoSvgTextShape *shape = selectedShape();
0544     if (shape) {
0545         KisHandlePainterHelper handlePainter =
0546             KoShape::createHandlePainterHelperView(&gc, shape, converter, handleRadius(), decorationThickness());
0547 
0548         if (m_dragging != DragMode::InlineSizeHandle && m_dragging != DragMode::Move) {
0549             handlePainter.setHandleStyle(KisHandleStyle::primarySelection());
0550             QPainterPath path;
0551             path.addRect(shape->outlineRect());
0552             handlePainter.drawPath(path);
0553         }
0554 
0555 
0556         qreal pxlToPt = canvas()->viewConverter()->viewToDocumentX(1.0);
0557         qreal length = (INLINE_SIZE_DASHES_PATTERN_A + INLINE_SIZE_DASHES_PATTERN_B) * INLINE_SIZE_DASHES_PATTERN_LENGTH;
0558         if (std::optional<InlineSizeInfo> info = InlineSizeInfo::fromShape(shape, length * pxlToPt)) {
0559             handlePainter.setHandleStyle(KisHandleStyle::secondarySelection());
0560             handlePainter.drawConnectionLine(info->baselineLineLocal());
0561 
0562             if (m_highlightItem == HighlightItem::InlineSizeStartHandle) {
0563                 handlePainter.setHandleStyle(m_dragging == DragMode::InlineSizeHandle? KisHandleStyle::partiallyHighlightedPrimaryHandles()
0564                                                                                      : KisHandleStyle::highlightedPrimaryHandles());
0565             }
0566             QVector<qreal> dashPattern = {INLINE_SIZE_DASHES_PATTERN_A, INLINE_SIZE_DASHES_PATTERN_B};
0567             handlePainter.drawHandleLine(info->startLineLocal());
0568             handlePainter.drawHandleLine(info->startLineDashes(), INLINE_SIZE_HANDLE_THICKNESS, dashPattern, INLINE_SIZE_DASHES_PATTERN_A);
0569 
0570             handlePainter.setHandleStyle(KisHandleStyle::secondarySelection());
0571             if (m_highlightItem == HighlightItem::InlineSizeEndHandle) {
0572                 handlePainter.setHandleStyle(m_dragging == DragMode::InlineSizeHandle? KisHandleStyle::partiallyHighlightedPrimaryHandles()
0573                                                                                      : KisHandleStyle::highlightedPrimaryHandles());
0574             }
0575             handlePainter.drawHandleLine(info->endLineLocal());
0576             handlePainter.drawHandleLine(info->endLineDashes(), INLINE_SIZE_HANDLE_THICKNESS, dashPattern, INLINE_SIZE_DASHES_PATTERN_A);
0577         }
0578 
0579         if (m_highlightItem == HighlightItem::MoveBorder) {
0580             handlePainter.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles());
0581         } else {
0582             handlePainter.setHandleStyle(KisHandleStyle::primarySelection());
0583         }
0584         handlePainter.drawHandleCircle(QPointF(), KoToolBase::handleRadius() * 0.75);
0585     }
0586 
0587     gc.setTransform(converter.documentToView(), true);
0588     {
0589         KisHandlePainterHelper handlePainter(&gc, handleRadius(), decorationThickness());
0590         if (!m_hoveredShapeHighlightRect.isEmpty()) {
0591             handlePainter.setHandleStyle(KisHandleStyle::highlightedPrimaryHandlesWithSolidOutline());
0592             QPainterPath path;
0593             path.addPath(m_hoveredShapeHighlightRect);
0594             handlePainter.drawPath(path);
0595         }
0596     }
0597     if (shape) {
0598             m_textCursor.paintDecorations(gc, qApp->palette().color(QPalette::Highlight), decorationThickness());
0599     }
0600     if (m_interactionStrategy) {
0601         gc.save();
0602         canvas()->snapGuide()->paint(gc, converter);
0603         gc.restore();
0604     }
0605 
0606     // Paint debug outline
0607     if (debugEnabled() && shape) {
0608         gc.save();
0609         using Element = KoSvgTextShape::DebugElement;
0610         KoSvgTextShape::DebugElements el{};
0611         if (optionUi.chkDbgCharBbox->isChecked()) {
0612             el |= Element::CharBbox;
0613         }
0614         if (optionUi.chkDbgLineBox->isChecked()) {
0615             el |= Element::LineBox;
0616         }
0617 
0618         gc.setTransform(shape->absoluteTransformation(), true);
0619         shape->paintDebug(gc, el);
0620         gc.restore();
0621     }
0622 }
0623 
0624 void SvgTextTool::mousePressEvent(KoPointerEvent *event)
0625 {
0626     KoSvgTextShape *selectedShape = this->selectedShape();
0627 
0628     if (selectedShape) {
0629         if (m_highlightItem == HighlightItem::MoveBorder) {
0630             m_interactionStrategy.reset(new SvgMoveTextStrategy(this, selectedShape, event->point));
0631             m_dragging = DragMode::Move;
0632             event->accept();
0633             return;
0634         } else if (m_highlightItem == HighlightItem::InlineSizeEndHandle) {
0635             m_interactionStrategy.reset(new SvgInlineSizeChangeStrategy(this, selectedShape, event->point, false));
0636             m_dragging = DragMode::InlineSizeHandle;
0637             event->accept();
0638             return;
0639         }  else if (m_highlightItem == HighlightItem::InlineSizeStartHandle) {
0640             m_interactionStrategy.reset(new SvgInlineSizeChangeStrategy(this, selectedShape, event->point, true));
0641             m_dragging = DragMode::InlineSizeHandle;
0642             event->accept();
0643             return;
0644         }
0645     }
0646 
0647     KoSvgTextShape *hoveredShape = dynamic_cast<KoSvgTextShape *>(canvas()->shapeManager()->shapeAt(event->point));
0648     QString shapeType;
0649     QPainterPath hoverPath = KisToolUtils::shapeHoverInfoCrossLayer(canvas(), event->point, shapeType);
0650     bool crossLayerPossible = !hoverPath.isEmpty() && shapeType == KoSvgTextShape_SHAPEID;
0651 
0652     if (!selectedShape && !hoveredShape && !crossLayerPossible) {
0653         QPointF point = canvas()->snapGuide()->snap(event->point, event->modifiers());
0654         m_interactionStrategy.reset(new SvgCreateTextStrategy(this, point));
0655         m_dragging = DragMode::Create;
0656         event->accept();
0657     } else if (hoveredShape) {
0658         if (hoveredShape != selectedShape) {
0659             canvas()->shapeManager()->selection()->deselectAll();
0660             canvas()->shapeManager()->selection()->select(hoveredShape);
0661             m_hoveredShapeHighlightRect = QPainterPath();
0662         }
0663         m_interactionStrategy.reset(new SvgSelectTextStrategy(this, &m_textCursor, event->point));
0664         m_dragging = DragMode::Select;
0665         event->accept();
0666     } else if (crossLayerPossible) {
0667         if (KisToolUtils::selectShapeCrossLayer(canvas(), event->point, KoSvgTextShape_SHAPEID)) {
0668             m_interactionStrategy.reset(new SvgSelectTextStrategy(this, &m_textCursor, event->point));
0669             m_dragging = DragMode::Select;
0670             m_hoveredShapeHighlightRect = QPainterPath();
0671         } else {
0672             canvas()->shapeManager()->selection()->deselectAll();
0673         }
0674         event->accept();
0675     } else { // if there's a selected shape but no hovered shape...
0676         canvas()->shapeManager()->selection()->deselectAll();
0677         event->accept();
0678     }
0679 
0680     repaintDecorations();
0681 }
0682 
0683 static inline Qt::CursorShape angleToCursor(const QVector2D unit)
0684 {
0685     constexpr float SIN_PI_8 = 0.382683432;
0686     if (unit.y() < SIN_PI_8 && unit.y() > -SIN_PI_8) {
0687         return Qt::SizeHorCursor;
0688     } else if (unit.x() < SIN_PI_8 && unit.x() > -SIN_PI_8) {
0689         return Qt::SizeVerCursor;
0690     } else if ((unit.x() > 0 && unit.y() > 0) || (unit.x() < 0 && unit.y() < 0)) {
0691         return Qt::SizeFDiagCursor;
0692     } else {
0693         return Qt::SizeBDiagCursor;
0694     }
0695 }
0696 
0697 static inline Qt::CursorShape lineToCursor(const QLineF line, const KoCanvasBase *const canvas)
0698 {
0699     const KisCanvas2 *const canvas2 = qobject_cast<const KisCanvas2 *>(canvas);
0700     KIS_ASSERT(canvas2);
0701     const KisCoordinatesConverter *const converter = canvas2->coordinatesConverter();
0702     QLineF wdgLine = converter->flakeToWidget(line);
0703     return angleToCursor(QVector2D(wdgLine.p2() - wdgLine.p1()).normalized());
0704 }
0705 
0706 void SvgTextTool::mouseMoveEvent(KoPointerEvent *event)
0707 {
0708     m_lastMousePos = event->point;
0709     m_hoveredShapeHighlightRect = QPainterPath();
0710 
0711 
0712     if (m_interactionStrategy) {
0713         m_interactionStrategy->handleMouseMove(event->point, event->modifiers());
0714         if (m_dragging == DragMode::Create) {
0715             SvgCreateTextStrategy *c = dynamic_cast<SvgCreateTextStrategy*>(m_interactionStrategy.get());
0716             if (c && c->draggingInlineSize()) {
0717                 if (this->writingMode() == KoSvgText::HorizontalTB) {
0718                     useCursor(m_text_inline_horizontal);
0719                 } else {
0720                     useCursor(m_text_inline_vertical);
0721                 }
0722             } else {
0723                 useCursor(m_base_cursor);
0724             }
0725         } else if (m_dragging == DragMode::Select && this->selectedShape()) {
0726             KoSvgTextShape *const selectedShape = this->selectedShape();
0727             // Todo: replace with something a little less hacky.
0728             if (selectedShape->writingMode() == KoSvgText::HorizontalTB) {
0729                 useCursor(m_ibeam_horizontal);
0730             } else {
0731                 useCursor(m_ibeam_vertical);
0732             }
0733         }
0734         event->accept();
0735     } else {
0736         m_highlightItem = HighlightItem::None;
0737         KoSvgTextShape *const selectedShape = this->selectedShape();
0738         QCursor cursor = m_base_cursor;
0739         if (selectedShape) {
0740             cursor = m_ibeam_horizontal_done;
0741             const qreal sensitivity = grabSensitivityInPt();
0742 
0743             if (std::optional<InlineSizeInfo> info = InlineSizeInfo::fromShape(selectedShape)) {
0744                 const QPolygonF zone = info->endLineGrabRect(sensitivity);
0745                 const QPolygonF startZone = info->startLineGrabRect(sensitivity);
0746                 if (zone.containsPoint(event->point, Qt::OddEvenFill)) {
0747                     m_highlightItem = HighlightItem::InlineSizeEndHandle;
0748                     cursor = lineToCursor(info->baselineLine(), canvas());
0749                 } else if (startZone.containsPoint(event->point, Qt::OddEvenFill)){
0750                     m_highlightItem = HighlightItem::InlineSizeStartHandle;
0751                     cursor = lineToCursor(info->baselineLine(), canvas());
0752                 }
0753             }
0754 
0755             if (m_highlightItem == HighlightItem::None) {
0756                 const QPolygonF textOutline = selectedShape->absoluteTransformation().map(selectedShape->outlineRect());
0757                 const QPolygonF moveBorderRegion = selectedShape->absoluteTransformation().map(kisGrowRect(selectedShape->outlineRect(),
0758                                                                                                            sensitivity * 2));
0759                 if (moveBorderRegion.containsPoint(event->point, Qt::OddEvenFill) && !textOutline.containsPoint(event->point, Qt::OddEvenFill)) {
0760                     m_highlightItem = HighlightItem::MoveBorder;
0761                     cursor = Qt::SizeAllCursor;
0762                 }
0763             }
0764         }
0765 
0766         QString shapeType;
0767         bool isHorizontal = true;
0768         const KoSvgTextShape *hoveredShape = dynamic_cast<KoSvgTextShape *>(canvas()->shapeManager()->shapeAt(event->point));
0769         QPainterPath hoverPath = KisToolUtils::shapeHoverInfoCrossLayer(canvas(), event->point, shapeType, &isHorizontal);
0770         if (selectedShape && selectedShape == hoveredShape && m_highlightItem == HighlightItem::None) {
0771             if (selectedShape->writingMode() == KoSvgText::HorizontalTB) {
0772                 cursor = m_ibeam_horizontal;
0773             } else {
0774                 cursor = m_ibeam_vertical;
0775             }
0776         } else if (hoveredShape) {
0777             if (!hoveredShape->shapesInside().isEmpty()) {
0778                 QPainterPath paths;
0779                 Q_FOREACH(KoShape *s, hoveredShape->shapesInside()) {
0780                     KoPathShape *path = dynamic_cast<KoPathShape *>(s);
0781                     if (path) {
0782                         paths.addPath(hoveredShape->absoluteTransformation().map(path->absoluteTransformation().map(path->outline())));
0783                     }
0784                 }
0785                 if (!paths.isEmpty()) {
0786                     m_hoveredShapeHighlightRect = paths;
0787                 }
0788             } else {
0789                 m_hoveredShapeHighlightRect.addRect(hoveredShape->boundingRect());
0790             }
0791             if (hoveredShape->writingMode() == KoSvgText::HorizontalTB) {
0792                 cursor = m_ibeam_horizontal;
0793             } else {
0794                 cursor = m_ibeam_vertical;
0795             }
0796         } else if (!hoverPath.isEmpty() && shapeType == KoSvgTextShape_SHAPEID && m_highlightItem == HighlightItem::None) {
0797             m_hoveredShapeHighlightRect = hoverPath;
0798             if (isHorizontal) {
0799                 cursor = m_ibeam_horizontal;
0800             } else {
0801                 cursor = m_ibeam_vertical;
0802             }
0803         }
0804 #if 0
0805         /// Commenting this out until we have a good idea of how we want to tackle the text and shape to put them on.
0806            else if(m_highlightItem == HighlightItem::None) {
0807             KoPathShape *shape = dynamic_cast<KoPathShape *>(canvas()->shapeManager()->shapeAt(event->point));
0808             if (shape) {
0809                 if (shape->subpathCount() > 0) {
0810                     if (shape->isClosedSubpath(0)) {
0811                         cursor = m_text_in_shape;
0812                     }
0813                 }
0814                 KoPathSegment segment = segmentAtPoint(event->point, shape, handleGrabRect(event->point));
0815                 if (segment.isValid()) {
0816                     cursor = m_text_on_path;
0817                 }
0818                 m_hoveredShapeHighlightRect.addPath(shape->absoluteTransformation().map(shape->outline()));
0819             } else {
0820                 m_hoveredShapeHighlightRect = QPainterPath();
0821             }
0822         }
0823 #endif
0824         useCursor(cursor);
0825         event->ignore();
0826     }
0827 
0828     repaintDecorations();
0829 }
0830 
0831 void SvgTextTool::mouseReleaseEvent(KoPointerEvent *event)
0832 {
0833     if (m_interactionStrategy) {
0834         m_interactionStrategy->finishInteraction(event->modifiers());
0835         KUndo2Command *const command = m_interactionStrategy->createCommand();
0836         if (command) {
0837             m_strategyAddingCommand = true;
0838             canvas()->addCommand(command);
0839             m_strategyAddingCommand = false;
0840         }
0841         m_interactionStrategy = nullptr;
0842         if (m_dragging != DragMode::Select) {
0843             useCursor(m_base_cursor);
0844         }
0845         m_dragging = DragMode::None;
0846         event->accept();
0847     } else {
0848         useCursor(m_base_cursor);
0849     }
0850     event->accept();
0851 }
0852 
0853 void SvgTextTool::keyPressEvent(QKeyEvent *event)
0854 {
0855     if (m_interactionStrategy
0856         && (event->key() == Qt::Key_Control || event->key() == Qt::Key_Alt || event->key() == Qt::Key_Shift
0857             || event->key() == Qt::Key_Meta)) {
0858         m_interactionStrategy->handleMouseMove(m_lastMousePos, event->modifiers());
0859         event->accept();
0860         return;
0861     } else if (event->key() == Qt::Key_Escape) {
0862         requestStrokeEnd();
0863     } else if (selectedShape()) {
0864         m_textCursor.keyPressEvent(event);
0865     }
0866 
0867     event->ignore();
0868 }
0869 
0870 void SvgTextTool::keyReleaseEvent(QKeyEvent *event)
0871 {
0872     if (m_interactionStrategy
0873         && (event->key() == Qt::Key_Control || event->key() == Qt::Key_Alt || event->key() == Qt::Key_Shift
0874             || event->key() == Qt::Key_Meta)) {
0875         m_interactionStrategy->handleMouseMove(m_lastMousePos, event->modifiers());
0876         event->accept();
0877     } else {
0878         event->ignore();
0879     }
0880 }
0881 
0882 void SvgTextTool::focusInEvent(QFocusEvent *event)
0883 {
0884     m_textCursor.focusIn();
0885     event->accept();
0886 }
0887 
0888 void SvgTextTool::focusOutEvent(QFocusEvent *event)
0889 {
0890     m_textCursor.focusOut();
0891     event->accept();
0892 }
0893 
0894 void SvgTextTool::mouseDoubleClickEvent(KoPointerEvent *event)
0895 {
0896     if (canvas()->shapeManager()->shapeAt(event->point) != selectedShape()) {
0897         event->ignore(); // allow the event to be used by another
0898         return;
0899     } else {
0900         m_textCursor.setPosToPoint(event->point, true);
0901         m_textCursor.moveCursor(SvgTextCursor::MoveWordLeft, true);
0902         m_textCursor.moveCursor(SvgTextCursor::MoveWordRight, false);
0903     }
0904     const QRectF updateRect = std::exchange(m_hoveredShapeHighlightRect, QPainterPath()).boundingRect();
0905     canvas()->updateCanvas(kisGrowRect(updateRect, 100));
0906     event->accept();
0907 }
0908 
0909 void SvgTextTool::mouseTripleClickEvent(KoPointerEvent *event)
0910 {
0911     if (canvas()->shapeManager()->shapeAt(event->point) == selectedShape()) {
0912         // TODO: Consider whether we want to use sentence based selection instead:
0913         // QTextBoundaryFinder allows us to find sentences if necessary.
0914         m_textCursor.moveCursor(SvgTextCursor::ParagraphStart, true);
0915         m_textCursor.moveCursor(SvgTextCursor::ParagraphEnd, false);
0916         event->accept();
0917     }
0918 }
0919 
0920 qreal SvgTextTool::grabSensitivityInPt() const
0921 {
0922     const int sensitivity = grabSensitivity();
0923     return canvas()->viewConverter()->viewToDocumentX(sensitivity);
0924 }
0925