diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index b4443e012e..45c1d087b1 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1253 +1,1255 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn); if (doc.isNull()) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); debugFlake << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { debugFlake << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { debugFlake << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line debugFlake << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; - QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction").split(" "); + QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction letter-spacing").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { debugFlake << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { debugFlake << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); // disable auto-formatting to avoid axtra spaces appearing here and there svgWriter.setAutoFormatting(false); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape * (according to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; boost::optional previousBlockAbsoluteXOffset = boost::none; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) boost::optional blockAbsoluteXOffset = boost::none; if (elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } blockAbsoluteXOffset = KisDomUtils::toDouble(xString); } if (previousBlockAbsoluteXOffset && blockAbsoluteXOffset && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) && svgReader.name() != "text" && elementAttributes.hasAttribute("dy")) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } if (elementAttributes.hasAttribute("x")) { previousBlockAbsoluteXOffset = blockAbsoluteXOffset; } } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() > 0, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; SvgGraphicsContext *context = new SvgGraphicsContext(); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) if (it.value() == test.property(it.key())) { // Some props must not be removed as default state gets in the way. if (it.key() == 0x2023) { // TextUnderlineStyle continue; + } else if (it.key() == 0x2033) { // FontLetterSpacingType + continue; } diff.clearProperty(it.key()); } return diff; } diff --git a/plugins/tools/svgtexttool/SvgTextEditor.cpp b/plugins/tools/svgtexttool/SvgTextEditor.cpp index 680df71bac..120e598608 100644 --- a/plugins/tools/svgtexttool/SvgTextEditor.cpp +++ b/plugins/tools/svgtexttool/SvgTextEditor.cpp @@ -1,1149 +1,1184 @@ /* This file is part of the KDE project * * Copyright 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgTextEditor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_font_family_combo_box.h" #include "FontSizeAction.h" #include "kis_signals_blocker.h" SvgTextEditor::SvgTextEditor(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags) , m_page(new QWidget(this)) #ifndef Q_OS_WIN , m_charSelectDialog(new KoDialog(this)) #endif { m_textEditorWidget.setupUi(m_page); setCentralWidget(m_page); m_textEditorWidget.chkVertical->setVisible(false); #ifndef Q_OS_WIN KCharSelect *charSelector = new KCharSelect(m_charSelectDialog, 0, KCharSelect::AllGuiElements); m_charSelectDialog->setMainWidget(charSelector); connect(charSelector, SIGNAL(currentCharChanged(QChar)), SLOT(insertCharacter(QChar))); m_charSelectDialog->hide(); m_charSelectDialog->setButtons(KoDialog::Close); #endif connect(m_textEditorWidget.buttons, SIGNAL(accepted()), this, SLOT(save())); connect(m_textEditorWidget.buttons, SIGNAL(rejected()), this, SLOT(slotCloseEditor())); connect(m_textEditorWidget.buttons, SIGNAL(clicked(QAbstractButton*)), this, SLOT(dialogButtonClicked(QAbstractButton*))); KConfigGroup cg(KSharedConfig::openConfig(), "SvgTextTool"); actionCollection()->setConfigGroup("SvgTextTool"); actionCollection()->setComponentName("svgtexttool"); actionCollection()->setComponentDisplayName(i18n("Text Tool")); QByteArray state; if (cg.hasKey("WindowState")) { state = cg.readEntry("State", state); state = QByteArray::fromBase64(state); // One day will need to load the version number, but for now, assume 0 restoreState(state); } setAcceptDrops(true); //setStandardToolBarMenuEnabled(true); #ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); #endif setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); m_syntaxHighlighter = new BasicXMLSyntaxHighlighter(m_textEditorWidget.svgTextEdit); m_textEditorWidget.svgTextEdit->setFont(QFontDatabase().systemFont(QFontDatabase::FixedFont)); createActions(); // If we have customized the toolbars, load that first setLocalXMLFile(KoResourcePaths::locateLocal("data", "svgtexttool.xmlgui")); setXMLFile(":/kxmlgui5/svgtexttool.xmlgui"); guiFactory()->addClient(this); // Create and plug toolbar list for Settings menu QList toolbarList; Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) { KToolBar * toolBar = ::qobject_cast(it); if (toolBar) { toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); KToggleAction* act = new KToggleAction(i18n("Show %1 Toolbar", toolBar->windowTitle()), this); actionCollection()->addAction(toolBar->objectName().toUtf8(), act); act->setCheckedState(KGuiItem(i18n("Hide %1 Toolbar", toolBar->windowTitle()))); connect(act, SIGNAL(toggled(bool)), this, SLOT(slotToolbarToggled(bool))); act->setChecked(!toolBar->isHidden()); toolbarList.append(act); } } plugActionList("toolbarlist", toolbarList); connect(m_textEditorWidget.textTab, SIGNAL(currentChanged(int)), this, SLOT(switchTextEditorTab())); switchTextEditorTab(); m_textEditorWidget.richTextEdit->document()->setDefaultStyleSheet("p {margin:0px;}"); applySettings(); } SvgTextEditor::~SvgTextEditor() { KConfigGroup g(KSharedConfig::openConfig(), "SvgTextTool"); QByteArray ba = saveState(); g.writeEntry("windowState", ba.toBase64()); } void SvgTextEditor::setShape(KoSvgTextShape *shape) { m_shape = shape; if (m_shape) { KoSvgTextShapeMarkupConverter converter(m_shape); QString svg; QString styles; QTextDocument *doc = m_textEditorWidget.richTextEdit->document(); if (converter.convertToSvg(&svg, &styles)) { m_textEditorWidget.svgTextEdit->setPlainText(svg); m_textEditorWidget.svgStylesEdit->setPlainText(styles); m_textEditorWidget.svgTextEdit->document()->setModified(false); if (shape->isRichTextPreferred() && converter.convertSvgToDocument(svg, doc)) { m_textEditorWidget.richTextEdit->setDocument(doc); KisSignalsBlocker b(m_textEditorWidget.textTab); m_textEditorWidget.textTab->setCurrentIndex(Richtext); doc->clearUndoRedoStacks(); switchTextEditorTab(false); } else { KisSignalsBlocker b(m_textEditorWidget.textTab); m_textEditorWidget.textTab->setCurrentIndex(SvgSource); switchTextEditorTab(false); } } else { QMessageBox::warning(this, i18n("Conversion failed"), "Could not get svg text from the shape:\n" + converter.errors().join('\n') + "\n" + converter.warnings().join('\n')); } } } void SvgTextEditor::save() { if (m_shape) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QString svg; QString styles = m_textEditorWidget.svgStylesEdit->document()->toPlainText(); KoSvgTextShapeMarkupConverter converter(m_shape); if (!converter.convertDocumentToSvg(m_textEditorWidget.richTextEdit->document(), &svg)) { qWarning()<<"new converter doesn't work!"; } m_textEditorWidget.richTextEdit->document()->setModified(false); emit textUpdated(m_shape, svg, styles, true); } else { emit textUpdated(m_shape, m_textEditorWidget.svgTextEdit->document()->toPlainText(), m_textEditorWidget.svgStylesEdit->document()->toPlainText(), false); m_textEditorWidget.svgTextEdit->document()->setModified(false); } } } void SvgTextEditor::switchTextEditorTab(bool convertData) { KoSvgTextShape shape; KoSvgTextShapeMarkupConverter converter(&shape); if (m_currentEditor) { disconnect(m_currentEditor->document(), SIGNAL(modificationChanged(bool)), this, SLOT(setModified(bool))); } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { //first, make buttons checkable enableRichTextActions(true); enableSvgTextActions(false); //then connect the cursor change to the checkformat(); connect(m_textEditorWidget.richTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(checkFormat())); if (m_shape && convertData) { QTextDocument *doc = m_textEditorWidget.richTextEdit->document(); if (!converter.convertSvgToDocument(m_textEditorWidget.svgTextEdit->document()->toPlainText(), doc)) { qWarning()<<"new converter svgToDoc doesn't work!"; } m_textEditorWidget.richTextEdit->setDocument(doc); doc->clearUndoRedoStacks(); } m_currentEditor = m_textEditorWidget.richTextEdit; } else { //first, make buttons uncheckable enableRichTextActions(false); enableSvgTextActions(true); disconnect(m_textEditorWidget.richTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(checkFormat())); // Convert the rich text to svg and styles strings if (m_shape && convertData) { QString svg; QString styles; if (!converter.convertDocumentToSvg(m_textEditorWidget.richTextEdit->document(), &svg)) { qWarning()<<"new converter docToSVG doesn't work!"; } m_textEditorWidget.svgTextEdit->setPlainText(svg); } m_currentEditor = m_textEditorWidget.svgTextEdit; } connect(m_currentEditor->document(), SIGNAL(modificationChanged(bool)), SLOT(setModified(bool))); } void SvgTextEditor::checkFormat() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); QTextBlockFormat blockFormat = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); // checkboxes do not emit signals on manual switching, so we // can avoid blocking them if (format.fontWeight() > QFont::Normal) { actionCollection()->action("svg_weight_bold")->setChecked(true); } else { actionCollection()->action("svg_weight_bold")->setChecked(false); } actionCollection()->action("svg_format_italic")->setChecked(format.fontItalic()); actionCollection()->action("svg_format_underline")->setChecked(format.fontUnderline()); actionCollection()->action("svg_format_strike_through")->setChecked(format.fontStrikeOut()); { FontSizeAction *fontSizeAction = qobject_cast(actionCollection()->action("svg_font_size")); KisSignalsBlocker b(fontSizeAction); fontSizeAction->setFontSize(format.font().pointSize()); } { KoColor fg(format.foreground().color(), KoColorSpaceRegistry::instance()->rgb8()); KoColorPopupAction *fgColorPopup = qobject_cast(actionCollection()->action("svg_format_textcolor")); KisSignalsBlocker b(fgColorPopup); fgColorPopup->setCurrentColor(fg); } { KoColor bg(format.foreground().color(), KoColorSpaceRegistry::instance()->rgb8()); KoColorPopupAction *bgColorPopup = qobject_cast(actionCollection()->action("svg_background_color")); KisSignalsBlocker b(bgColorPopup); bgColorPopup->setCurrentColor(bg); } { KisFontComboBoxes* fontComboBox = qobject_cast(qobject_cast(actionCollection()->action("svg_font"))->defaultWidget()); KisSignalsBlocker b(fontComboBox); fontComboBox->setCurrentFont(format.font()); } { QDoubleSpinBox *spnLineHeight = qobject_cast(qobject_cast(actionCollection()->action("svg_line_height"))->defaultWidget()); KisSignalsBlocker b(spnLineHeight); if (blockFormat.lineHeightType() == QTextBlockFormat::SingleHeight) { spnLineHeight->setValue(100.0); } else if(blockFormat.lineHeightType() == QTextBlockFormat::ProportionalHeight) { spnLineHeight->setValue(double(blockFormat.lineHeight())); } } + + { + QDoubleSpinBox* spnLetterSpacing = qobject_cast(qobject_cast(actionCollection()->action("svg_letter_spacing"))->defaultWidget()); + KisSignalsBlocker b(spnLetterSpacing); + spnLetterSpacing->setValue(format.fontLetterSpacing()); + } } void SvgTextEditor::undo() { m_currentEditor->undo(); } void SvgTextEditor::redo() { m_currentEditor->redo(); } void SvgTextEditor::cut() { m_currentEditor->cut(); } void SvgTextEditor::copy() { m_currentEditor->copy(); } void SvgTextEditor::paste() { m_currentEditor->paste(); } void SvgTextEditor::selectAll() { m_currentEditor->selectAll(); } void SvgTextEditor::deselect() { QTextCursor cursor(m_currentEditor->textCursor()); cursor.clearSelection(); m_currentEditor->setTextCursor(cursor); } void SvgTextEditor::find() { QDialog *findDialog = new QDialog(this); findDialog->setWindowTitle(i18n("Find Text")); QFormLayout *layout = new QFormLayout(); findDialog->setLayout(layout); QLineEdit *lnSearchKey = new QLineEdit(); layout->addRow(i18n("Find:"), lnSearchKey); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); findDialog->layout()->addWidget(buttons); connect(buttons, SIGNAL(accepted()), findDialog, SLOT(accept())); connect(buttons, SIGNAL(rejected()), findDialog, SLOT(reject())); if (findDialog->exec()==QDialog::Accepted) { m_searchKey = lnSearchKey->text(); m_currentEditor->find(m_searchKey); } } void SvgTextEditor::findNext() { if (!m_currentEditor->find(m_searchKey)) { QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::Start); m_currentEditor->setTextCursor(cursor); m_currentEditor->find(m_searchKey); } } void SvgTextEditor::findPrev() { if (!m_currentEditor->find(m_searchKey,QTextDocument::FindBackward)) { QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::End); m_currentEditor->setTextCursor(cursor); m_currentEditor->find(m_searchKey,QTextDocument::FindBackward); } } void SvgTextEditor::replace() { QDialog *findDialog = new QDialog(this); findDialog->setWindowTitle(i18n("Find and Replace all")); QFormLayout *layout = new QFormLayout(); findDialog->setLayout(layout); QLineEdit *lnSearchKey = new QLineEdit(); QLineEdit *lnReplaceKey = new QLineEdit(); layout->addRow(i18n("Find:"), lnSearchKey); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); layout->addRow(i18n("Replace:"), lnReplaceKey); findDialog->layout()->addWidget(buttons); connect(buttons, SIGNAL(accepted()), findDialog, SLOT(accept())); connect(buttons, SIGNAL(rejected()), findDialog, SLOT(reject())); if (findDialog->exec()==QDialog::Accepted) { QString search = lnSearchKey->text(); QString replace = lnReplaceKey->text(); QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::Start); m_currentEditor->setTextCursor(cursor); while(m_currentEditor->find(search)) { m_currentEditor->textCursor().removeSelectedText(); m_currentEditor->textCursor().insertText(replace); } } } void SvgTextEditor::zoomOut() { m_currentEditor->zoomOut(); } void SvgTextEditor::zoomIn() { m_currentEditor->zoomIn(); } #ifndef Q_OS_WIN void SvgTextEditor::showInsertSpecialCharacterDialog() { m_charSelectDialog->setVisible(!m_charSelectDialog->isVisible()); } void SvgTextEditor::insertCharacter(const QChar &c) { m_currentEditor->textCursor().insertText(QString(c)); } #endif void SvgTextEditor::setTextBold(QFont::Weight weight) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; QTextCursor oldCursor = setTextSelection(); if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight() > QFont::Normal && weight==QFont::Bold) { format.setFontWeight(QFont::Normal); } else { format.setFontWeight(weight); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setTextWeightLight() { if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight() < QFont::Normal) { setTextBold(QFont::Normal); } else { setTextBold(QFont::Light); } } void SvgTextEditor::setTextWeightNormal() { setTextBold(QFont::Normal); } void SvgTextEditor::setTextWeightDemi() { if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight() != QFont::Normal) { setTextBold(QFont::Normal); } else { setTextBold(QFont::DemiBold); } } void SvgTextEditor::setTextWeightBlack() { if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight()>QFont::Normal) { setTextBold(QFont::Normal); } else { setTextBold(QFont::Black); } } void SvgTextEditor::setTextItalic(QFont::Style style) { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); QString fontStyle = "inherit"; if (style == QFont::StyleItalic) { fontStyle = "italic"; } else if(style == QFont::StyleOblique) { fontStyle = "oblique"; } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; QTextCursor origCursor = setTextSelection(); format.setFontItalic(!m_textEditorWidget.richTextEdit->textCursor().charFormat().fontItalic()); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(origCursor); } else { if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setTextDecoration(KoSvgText::TextDecoration decor) { QTextCursor cursor = setTextSelection(); QTextCharFormat currentFormat = m_textEditorWidget.richTextEdit->textCursor().charFormat(); QTextCharFormat format; QString textDecoration = "inherit"; if (decor == KoSvgText::DecorationUnderline) { textDecoration = "underline"; if (currentFormat.fontUnderline()) { format.setFontUnderline(false); } else { format.setFontUnderline(true); } format.setFontOverline(false); format.setFontStrikeOut(false); } else if (decor == KoSvgText::DecorationLineThrough) { textDecoration = "line-through"; format.setFontUnderline(false); format.setFontOverline(false); if (currentFormat.fontStrikeOut()) { format.setFontStrikeOut(false); } else { format.setFontStrikeOut(true); } } else if (decor == KoSvgText::DecorationOverline) { textDecoration = "overline"; format.setFontUnderline(false); if (currentFormat.fontOverline()) { format.setFontOverline(false); } else { format.setFontOverline(true); } format.setFontStrikeOut(false); } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } m_textEditorWidget.richTextEdit->setTextCursor(cursor); } void SvgTextEditor::setTextUnderline() { setTextDecoration(KoSvgText::DecorationUnderline); } void SvgTextEditor::setTextOverline() { setTextDecoration(KoSvgText::DecorationOverline); } void SvgTextEditor::setTextStrikethrough() { setTextDecoration(KoSvgText::DecorationLineThrough); } void SvgTextEditor::setTextSubscript() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); if (format.verticalAlignment()==QTextCharFormat::AlignSubScript) { format.setVerticalAlignment(QTextCharFormat::AlignNormal); } else { format.setVerticalAlignment(QTextCharFormat::AlignSubScript); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } void SvgTextEditor::setTextSuperScript() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); if (format.verticalAlignment()==QTextCharFormat::AlignSuperScript) { format.setVerticalAlignment(QTextCharFormat::AlignNormal); } else { format.setVerticalAlignment(QTextCharFormat::AlignSuperScript); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } void SvgTextEditor::increaseTextSize() { QTextCursor oldCursor = setTextSelection(); QTextCharFormat format; int pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pointSize(); if (pointSize<0) { pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pixelSize(); } format.setFontPointSize(pointSize+1.0); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::decreaseTextSize() { QTextCursor oldCursor = setTextSelection(); QTextCharFormat format; int pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pointSize(); if (pointSize<1) { pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pixelSize(); } format.setFontPointSize(qMax(pointSize-1.0, 1.0)); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::setLineHeight(double lineHeightPercentage) { QTextCursor oldCursor = setTextSelection(); QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setLineHeight(lineHeightPercentage, QTextBlockFormat::ProportionalHeight); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } +void SvgTextEditor::setLetterSpacing(double letterSpacing) +{ + QTextCursor cursor = setTextSelection(); + if (m_textEditorWidget.textTab->currentIndex() == Richtext) { + QTextCharFormat format; + format.setFontLetterSpacingType(QFont::AbsoluteSpacing); + format.setFontLetterSpacing(letterSpacing); + m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); + m_textEditorWidget.richTextEdit->setTextCursor(cursor); + } + else { + if (cursor.hasSelection()) { + QString selectionModified = "" + cursor.selectedText() + ""; + cursor.removeSelectedText(); + cursor.insertText(selectionModified); + } + } +} void SvgTextEditor::alignLeft() { QTextCursor oldCursor = setTextSelection(); QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignLeft); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::alignRight() { QTextCursor oldCursor = setTextSelection(); QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignRight); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::alignCenter() { QTextCursor oldCursor = setTextSelection(); QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignCenter); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::alignJustified() { QTextCursor oldCursor = setTextSelection(); QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignJustify); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } void SvgTextEditor::setSettings() { KoDialog settingsDialog(this); Ui_WdgSvgTextSettings textSettings; QWidget *settingsPage = new QWidget(&settingsDialog, 0); settingsDialog.setMainWidget(settingsPage); textSettings.setupUi(settingsPage); // get the settings and initialize the dialog KConfigGroup cfg(KSharedConfig::openConfig(), "SvgTextTool"); QStringList selectedWritingSystems = cfg.readEntry("selectedWritingSystems", "").split(","); QList scripts = QFontDatabase().writingSystems(); QStandardItemModel *writingSystemsModel = new QStandardItemModel(&settingsDialog); for (int s = 0; s < scripts.size(); s ++) { QString writingSystem = QFontDatabase().writingSystemName(scripts.at(s)); QStandardItem *script = new QStandardItem(writingSystem); script->setCheckable(true); script->setCheckState(selectedWritingSystems.contains(QString::number(scripts.at(s))) ? Qt::Checked : Qt::Unchecked); script->setData((int)scripts.at(s)); writingSystemsModel->appendRow(script); } textSettings.lwScripts->setModel(writingSystemsModel); EditorMode mode = (EditorMode)cfg.readEntry("EditorMode", (int)Both); switch(mode) { case(RichText): textSettings.radioRichText->setChecked(true); break; case(SvgSource): textSettings.radioSvgSource->setChecked(true); break; case(Both): textSettings.radioBoth->setChecked(true); } QColor background = cfg.readEntry("colorEditorBackground", qApp->palette().background().color()); textSettings.colorEditorBackground->setColor(background); textSettings.colorEditorForeground->setColor(cfg.readEntry("colorEditorForeground", qApp->palette().text().color())); textSettings.colorKeyword->setColor(cfg.readEntry("colorKeyword", QColor(background.value() < 100 ? Qt::cyan : Qt::blue))); textSettings.chkBoldKeyword->setChecked(cfg.readEntry("BoldKeyword", true)); textSettings.chkItalicKeyword->setChecked(cfg.readEntry("ItalicKeyword", false)); textSettings.colorElement->setColor(cfg.readEntry("colorElement", QColor(background.value() < 100 ? Qt::magenta : Qt::darkMagenta))); textSettings.chkBoldElement->setChecked(cfg.readEntry("BoldElement", true)); textSettings.chkItalicElement->setChecked(cfg.readEntry("ItalicElement", false)); textSettings.colorAttribute->setColor(cfg.readEntry("colorAttribute", QColor(background.value() < 100 ? Qt::green : Qt::darkGreen))); textSettings.chkBoldAttribute->setChecked(cfg.readEntry("BoldAttribute", true)); textSettings.chkItalicAttribute->setChecked(cfg.readEntry("ItalicAttribute", true)); textSettings.colorValue->setColor(cfg.readEntry("colorValue", QColor(background.value() < 100 ? Qt::red: Qt::darkRed))); textSettings.chkBoldValue->setChecked(cfg.readEntry("BoldValue", true)); textSettings.chkItalicValue->setChecked(cfg.readEntry("ItalicValue", false)); textSettings.colorComment->setColor(cfg.readEntry("colorComment", QColor(background.value() < 100 ? Qt::lightGray : Qt::gray))); textSettings.chkBoldComment->setChecked(cfg.readEntry("BoldComment", false)); textSettings.chkItalicComment->setChecked(cfg.readEntry("ItalicComment", false)); settingsDialog.setButtons(KoDialog::Ok | KoDialog::Cancel); if (settingsDialog.exec() == QDialog::Accepted) { // save and set the settings QStringList writingSystems; for (int i = 0; i < writingSystemsModel->rowCount(); i++) { QStandardItem *item = writingSystemsModel->item(i); if (item->checkState() == Qt::Checked) { writingSystems.append(QString::number(item->data().toInt())); } } cfg.writeEntry("selectedWritingSystems", writingSystems.join(',')); if (textSettings.radioRichText->isChecked()) { cfg.writeEntry("EditorMode", (int)Richtext); } else if (textSettings.radioSvgSource->isChecked()) { cfg.writeEntry("EditorMode", (int)SvgSource); } else if (textSettings.radioBoth->isChecked()) { cfg.writeEntry("EditorMode", (int)Both); } cfg.writeEntry("colorEditorBackground", textSettings.colorEditorBackground->color()); cfg.writeEntry("colorEditorForeground", textSettings.colorEditorForeground->color()); cfg.writeEntry("colorKeyword", textSettings.colorKeyword->color()); cfg.writeEntry("BoldKeyword", textSettings.chkBoldKeyword->isChecked()); cfg.writeEntry("ItalicKeyWord", textSettings.chkItalicKeyword->isChecked()); cfg.writeEntry("colorElement", textSettings.colorElement->color()); cfg.writeEntry("BoldElement", textSettings.chkBoldElement->isChecked()); cfg.writeEntry("ItalicElement", textSettings.chkItalicElement->isChecked()); cfg.writeEntry("colorAttribute", textSettings.colorAttribute->color()); cfg.writeEntry("BoldAttribute", textSettings.chkBoldAttribute->isChecked()); cfg.writeEntry("ItalicAttribute", textSettings.chkItalicAttribute->isChecked()); cfg.writeEntry("colorValue", textSettings.colorValue->color()); cfg.writeEntry("BoldValue", textSettings.chkBoldValue->isChecked()); cfg.writeEntry("ItalicValue", textSettings.chkItalicValue->isChecked()); cfg.writeEntry("colorComment", textSettings.colorComment->color()); cfg.writeEntry("BoldComment", textSettings.chkBoldComment->isChecked()); cfg.writeEntry("ItalicComment", textSettings.chkItalicComment->isChecked()); applySettings(); } } void SvgTextEditor::slotToolbarToggled(bool) { } void SvgTextEditor::setFontColor(const KoColor &c) { QColor color = c.toQColor(); if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCursor oldCursor = setTextSelection(); QTextCharFormat format; format.setForeground(QBrush(color)); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setBackgroundColor(const KoColor &c) { QColor color = c.toQColor(); QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } void SvgTextEditor::setModified(bool modified) { if (modified) { m_textEditorWidget.buttons->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Discard); } else { m_textEditorWidget.buttons->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Close); } } void SvgTextEditor::dialogButtonClicked(QAbstractButton *button) { if (m_textEditorWidget.buttons->standardButton(button) == QDialogButtonBox::Discard) { if (QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("You have modified the text. Discard changes?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { close(); } } } void SvgTextEditor::setFont(const QString &fontName) { QFont font; font.fromString(fontName); QTextCharFormat curFormat = m_textEditorWidget.richTextEdit->textCursor().charFormat(); font.setPointSize(curFormat.font().pointSize()); QTextCharFormat format; //This disables the style being set from the font-comboboxes too, so we need to rethink how we use that. format.setFontFamily(font.family()); if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCursor oldCursor = setTextSelection(); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setFontSize(qreal fontSize) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCursor oldCursor = setTextSelection(); QTextCharFormat format; format.setFontPointSize(fontSize); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); m_textEditorWidget.richTextEdit->setTextCursor(oldCursor); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setBaseline(KoSvgText::BaselineShiftMode) { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } void SvgTextEditor::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { int numDegrees = event->delta() / 8; int numSteps = numDegrees / 7; m_textEditorWidget.svgTextEdit->zoomOut(numSteps); event->accept(); } } QTextCursor SvgTextEditor::setTextSelection() { QTextCursor orignalCursor(m_textEditorWidget.richTextEdit->textCursor()); if (!orignalCursor.hasSelection()){ m_textEditorWidget.richTextEdit->selectAll(); } return orignalCursor; } void SvgTextEditor::applySettings() { KConfigGroup cfg(KSharedConfig::openConfig(), "SvgTextTool"); EditorMode mode = (EditorMode)cfg.readEntry("EditorMode", (int)Both); QWidget *richTab = m_textEditorWidget.richTab; QWidget *svgTab = m_textEditorWidget.svgTab; m_page->setUpdatesEnabled(false); m_textEditorWidget.textTab->clear(); switch(mode) { case(RichText): m_textEditorWidget.textTab->addTab(richTab, i18n("Rich text")); break; case(SvgSource): m_textEditorWidget.textTab->addTab(svgTab, i18n("SVG Source")); break; case(Both): m_textEditorWidget.textTab->addTab(richTab, i18n("Rich text")); m_textEditorWidget.textTab->addTab(svgTab, i18n("SVG Source")); } m_syntaxHighlighter->setFormats(); QPalette palette = m_textEditorWidget.svgTextEdit->palette(); QColor background = cfg.readEntry("colorEditorBackground", qApp->palette().background().color()); palette.setBrush(QPalette::Active, QPalette::Background, QBrush(background)); QColor foreground = cfg.readEntry("colorEditorForeground", qApp->palette().text().color()); palette.setBrush(QPalette::Active, QPalette::Text, QBrush(foreground)); QStringList selectedWritingSystems = cfg.readEntry("selectedWritingSystems", "").split(","); QVector writingSystems; for (int i=0; i(qobject_cast(actionCollection()->action("svg_font"))->defaultWidget())->refillComboBox(writingSystems); m_page->setUpdatesEnabled(true); } QAction *SvgTextEditor::createAction(const QString &name, const char *member) { QAction *action = new QAction(this); KisActionRegistry *actionRegistry = KisActionRegistry::instance(); actionRegistry->propertizeAction(name, action); actionCollection()->addAction(name, action); QObject::connect(action, SIGNAL(triggered(bool)), this, member); return action; } void SvgTextEditor::createActions() { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); // File: new, open, save, save as, close KStandardAction::save(this, SLOT(save()), actionCollection()); KStandardAction::close(this, SLOT(slotCloseEditor()), actionCollection()); // Edit KStandardAction::undo(this, SLOT(undo()), actionCollection()); KStandardAction::redo(this, SLOT(redo()), actionCollection()); KStandardAction::cut(this, SLOT(cut()), actionCollection()); KStandardAction::copy(this, SLOT(copy()), actionCollection()); KStandardAction::paste(this, SLOT(paste()), actionCollection()); KStandardAction::selectAll(this, SLOT(selectAll()), actionCollection()); KStandardAction::deselect(this, SLOT(deselect()), actionCollection()); KStandardAction::find(this, SLOT(find()), actionCollection()); KStandardAction::findNext(this, SLOT(findNext()), actionCollection()); KStandardAction::findPrev(this, SLOT(findPrev()), actionCollection()); KStandardAction::replace(this, SLOT(replace()), actionCollection()); // View // WISH: we cannot zoom-in/out in rech-text mode m_svgTextActions << KStandardAction::zoomOut(this, SLOT(zoomOut()), actionCollection()); m_svgTextActions << KStandardAction::zoomIn(this, SLOT(zoomIn()), actionCollection()); #ifndef Q_OS_WIN // Insert: QAction * insertAction = createAction("svg_insert_special_character", SLOT(showInsertSpecialCharacterDialog())); insertAction->setCheckable(true); insertAction->setChecked(false); #endif // Format: m_richTextActions << createAction("svg_weight_bold", SLOT(setTextBold())); m_richTextActions << createAction("svg_format_italic", SLOT(setTextItalic())); m_richTextActions << createAction("svg_format_underline", SLOT(setTextUnderline())); m_richTextActions << createAction("svg_format_strike_through", SLOT(setTextStrikethrough())); m_richTextActions << createAction("svg_format_superscript", SLOT(setTextSuperScript())); m_richTextActions << createAction("svg_format_subscript", SLOT(setTextSubscript())); m_richTextActions << createAction("svg_weight_light", SLOT(setTextWeightLight())); m_richTextActions << createAction("svg_weight_normal", SLOT(setTextWeightNormal())); m_richTextActions << createAction("svg_weight_demi", SLOT(setTextWeightDemi())); m_richTextActions << createAction("svg_weight_black", SLOT(setTextWeightBlack())); m_richTextActions << createAction("svg_increase_font_size", SLOT(increaseTextSize())); m_richTextActions << createAction("svg_decrease_font_size", SLOT(decreaseTextSize())); m_richTextActions << createAction("svg_align_left", SLOT(alignLeft())); m_richTextActions << createAction("svg_align_right", SLOT(alignRight())); m_richTextActions << createAction("svg_align_center", SLOT(alignCenter())); // m_richTextActions << createAction("svg_align_justified", // SLOT(alignJustified())); // Settings m_richTextActions << createAction("svg_settings", SLOT(setSettings())); QWidgetAction *fontComboAction = new QWidgetAction(this); fontComboAction->setToolTip(i18n("Font")); KisFontComboBoxes *fontCombo = new KisFontComboBoxes(); connect(fontCombo, SIGNAL(fontChanged(QString)), SLOT(setFont(QString))); fontComboAction->setDefaultWidget(fontCombo); actionCollection()->addAction("svg_font", fontComboAction); m_richTextActions << fontComboAction; actionRegistry->propertizeAction("svg_font", fontComboAction); QWidgetAction *fontSizeAction = new FontSizeAction(this); fontSizeAction->setToolTip(i18n("Size")); connect(fontSizeAction, SIGNAL(fontSizeChanged(qreal)), this, SLOT(setFontSize(qreal))); actionCollection()->addAction("svg_font_size", fontSizeAction); m_richTextActions << fontSizeAction; actionRegistry->propertizeAction("svg_font_size", fontSizeAction); KoColorPopupAction *fgColor = new KoColorPopupAction(this); fgColor->setCurrentColor(QColor(Qt::black)); fgColor->setToolTip(i18n("Text Color")); connect(fgColor, SIGNAL(colorChanged(KoColor)), SLOT(setFontColor(KoColor))); actionCollection()->addAction("svg_format_textcolor", fgColor); m_richTextActions << fgColor; actionRegistry->propertizeAction("svg_format_textcolor", fgColor); KoColorPopupAction *bgColor = new KoColorPopupAction(this); bgColor->setCurrentColor(QColor(Qt::white)); bgColor->setToolTip(i18n("Background Color")); connect(bgColor, SIGNAL(colorChanged(KoColor)), SLOT(setBackgroundColor(KoColor))); actionCollection()->addAction("svg_background_color", bgColor); actionRegistry->propertizeAction("svg_background_color", bgColor); m_richTextActions << bgColor; QWidgetAction *colorPickerAction = new QWidgetAction(this); colorPickerAction->setToolTip(i18n("Pick a Color")); KisScreenColorPicker *colorPicker = new KisScreenColorPicker(false); connect(colorPicker, SIGNAL(sigNewColorPicked(KoColor)), fgColor, SLOT(setCurrentColor(KoColor))); connect(colorPicker, SIGNAL(sigNewColorPicked(KoColor)), SLOT(setFontColor(KoColor))); colorPickerAction->setDefaultWidget(colorPicker); actionCollection()->addAction("svg_pick_color", colorPickerAction); m_richTextActions << colorPickerAction; actionRegistry->propertizeAction("svg_pick_color", colorPickerAction); QWidgetAction *lineHeight = new QWidgetAction(this); QDoubleSpinBox *spnLineHeight = new QDoubleSpinBox(); spnLineHeight->setToolTip(i18n("Line height")); spnLineHeight->setRange(0.0, 1000.0); spnLineHeight->setSingleStep(10.0); spnLineHeight->setSuffix("%"); connect(spnLineHeight, SIGNAL(valueChanged(double)), SLOT(setLineHeight(double))); lineHeight->setDefaultWidget(spnLineHeight); actionCollection()->addAction("svg_line_height", lineHeight); m_richTextActions << lineHeight; actionRegistry->propertizeAction("svg_line_height", lineHeight); + + QWidgetAction *letterSpacing = new QWidgetAction(this); + QDoubleSpinBox *spnletterSpacing = new QDoubleSpinBox(); + spnletterSpacing->setToolTip(i18n("Letter Spacing")); + spnletterSpacing->setRange(-20.0, 20.0); + spnletterSpacing->setSingleStep(0.5); + connect(spnletterSpacing, SIGNAL(valueChanged(double)), SLOT(setLetterSpacing(double))); + letterSpacing->setDefaultWidget(spnletterSpacing); + actionCollection()->addAction("svg_letter_spacing", letterSpacing); + m_richTextActions << letterSpacing; + actionRegistry->propertizeAction("svg_letter_spacing", letterSpacing); } void SvgTextEditor::enableRichTextActions(bool enable) { Q_FOREACH(QAction *action, m_richTextActions) { action->setEnabled(enable); } } void SvgTextEditor::enableSvgTextActions(bool enable) { Q_FOREACH(QAction *action, m_svgTextActions) { action->setEnabled(enable); } } void SvgTextEditor::slotCloseEditor() { close(); emit textEditorClosed(); } diff --git a/plugins/tools/svgtexttool/SvgTextEditor.h b/plugins/tools/svgtexttool/SvgTextEditor.h index 064156b25c..1e2a277340 100644 --- a/plugins/tools/svgtexttool/SvgTextEditor.h +++ b/plugins/tools/svgtexttool/SvgTextEditor.h @@ -1,168 +1,169 @@ /* This file is part of the KDE project * * Copyright 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef TEXTNGSHAPECONFIGWIDGET_H #define TEXTNGSHAPECONFIGWIDGET_H #include #include #include #include #include //for the enums #include #include "ui_WdgSvgTextEditor.h" #include "ui_WdgSvgTextSettings.h" class KoSvgTextShape; class KoDialog; class SvgTextEditor : public KXmlGuiWindow { Q_OBJECT public: SvgTextEditor(QWidget *parent = 0, Qt::WindowFlags flags = 0); ~SvgTextEditor(); //tiny enum to keep track of the tab on which editor something happens while keeping the code readable. enum Editor { Richtext, // 0 SVGsource // 1 }; // enum to store which tabs are visible in the configuration enum EditorMode { RichText, SvgSource, Both }; void setShape(KoSvgTextShape *shape); private Q_SLOTS: /** * switch the text editor tab. */ void switchTextEditorTab(bool convertData = true); void slotCloseEditor(); /** * in rich text, check the current format, and toggle the given buttons. */ void checkFormat(); void save(); void undo(); void redo(); void cut(); void copy(); void paste(); void selectAll(); void deselect(); void find(); void findNext(); void findPrev(); void replace(); void zoomOut(); void zoomIn(); #ifndef Q_OS_WIN void showInsertSpecialCharacterDialog(); void insertCharacter(const QChar &c); #endif void setTextBold(QFont::Weight weight = QFont::Bold); void setTextWeightLight(); void setTextWeightNormal(); void setTextWeightDemi(); void setTextWeightBlack(); void setTextItalic(QFont::Style style = QFont::StyleOblique); void setTextDecoration(KoSvgText::TextDecoration decor); void setTextUnderline(); void setTextOverline(); void setTextStrikethrough(); void setTextSubscript(); void setTextSuperScript(); void increaseTextSize(); void decreaseTextSize(); void setLineHeight(double lineHeightPercentage); + void setLetterSpacing(double letterSpacing); void alignLeft(); void alignRight(); void alignCenter(); void alignJustified(); void setFont(const QString &fontName); void setFontSize(qreal size); void setBaseline(KoSvgText::BaselineShiftMode baseline); void setSettings(); void slotToolbarToggled(bool); void setFontColor(const KoColor &c); void setBackgroundColor(const KoColor &c); void setModified(bool modified); void dialogButtonClicked(QAbstractButton *button); Q_SIGNALS: void textUpdated(KoSvgTextShape *shape, const QString &svg, const QString &defs, bool richTextPreferred); void textEditorClosed(); protected: void wheelEvent(QWheelEvent *event) override; /** * Selects all if there is no selection * @returns a copy of the previous cursor. */ QTextCursor setTextSelection(); private: void applySettings(); QAction *createAction(const QString &name, const char *member); void createActions(); void enableRichTextActions(bool enable); void enableSvgTextActions(bool enable); Ui_WdgSvgTextEditor m_textEditorWidget; QTextEdit *m_currentEditor {0}; QWidget *m_page {0}; QList m_richTextActions; QList m_svgTextActions; KoSvgTextShape *m_shape {0}; #ifndef Q_OS_WIN KoDialog *m_charSelectDialog {0}; #endif BasicXMLSyntaxHighlighter *m_syntaxHighlighter; QString m_searchKey; }; #endif //TEXTNGSHAPECONFIGWIDGET_H diff --git a/plugins/tools/svgtexttool/svgtexttool.xmlgui b/plugins/tools/svgtexttool/svgtexttool.xmlgui index 026b8d0982..16fe4f94e0 100644 --- a/plugins/tools/svgtexttool/svgtexttool.xmlgui +++ b/plugins/tools/svgtexttool/svgtexttool.xmlgui @@ -1,100 +1,101 @@ &File &Edit &View &Insert &Format &Weight Setti&ngs File Font Settings Formatting +