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