Warning, file /education/cantor/src/textentry.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-License-Identifier: GPL-2.0-or-later 0003 SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com> 0004 SPDX-FileCopyrightText: 2012 Martin Kuettler <martin.kuettler@gmail.com> 0005 */ 0006 0007 #include "textentry.h" 0008 #include "lib/renderer.h" 0009 #include "latexrenderer.h" 0010 #include "lib/jupyterutils.h" 0011 #include "mathrender.h" 0012 #include "worksheetview.h" 0013 0014 #include "settings.h" 0015 0016 #include <QScopedPointer> 0017 #include <QGraphicsLinearLayout> 0018 #include <QJsonValue> 0019 #include <QJsonObject> 0020 #include <QJsonArray> 0021 #include <QDebug> 0022 #include <KLocalizedString> 0023 #include <KColorScheme> 0024 #include <QRegularExpression> 0025 #include <QStringList> 0026 #include <QInputDialog> 0027 #include <QActionGroup> 0028 0029 QStringList standartRawCellTargetNames = {QLatin1String("None"), QLatin1String("LaTeX"), QLatin1String("reST"), QLatin1String("HTML"), QLatin1String("Markdown")}; 0030 QStringList standartRawCellTargetMimes = {QString(), QLatin1String("text/latex"), QLatin1String("text/restructuredtext"), QLatin1String("text/html"), QLatin1String("text/markdown")}; 0031 0032 TextEntry::TextEntry(Worksheet* worksheet) : WorksheetEntry(worksheet) 0033 , m_rawCell(false) 0034 , m_convertTarget() 0035 , m_targetActionGroup(nullptr) 0036 , m_ownTarget{nullptr} 0037 , m_targetMenu(nullptr) 0038 , m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)) 0039 { 0040 m_textItem->enableRichText(true); 0041 0042 connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &TextEntry::moveToPreviousEntry); 0043 connect(m_textItem, &WorksheetTextItem::moveToNext, this, &TextEntry::moveToNextEntry); 0044 // Modern syntax of signal/stots don't work on this connection (arguments don't match) 0045 connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate())); 0046 connect(m_textItem, &WorksheetTextItem::doubleClick, this, &TextEntry::resolveImagesAtCursor); 0047 0048 // Init raw cell target menus 0049 // This used only for raw cells, but removing and creating this on conversion more complex 0050 // that just create them always 0051 m_targetActionGroup= new QActionGroup(this); 0052 m_targetActionGroup->setExclusive(true); 0053 connect(m_targetActionGroup, &QActionGroup::triggered, this, &TextEntry::convertTargetChanged); 0054 0055 m_targetMenu = new QMenu(i18n("Raw Cell Targets")); 0056 for (const QString& key : standartRawCellTargetNames) 0057 { 0058 QAction* action = new QAction(key, m_targetActionGroup); 0059 action->setCheckable(true); 0060 m_targetMenu->addAction(action); 0061 } 0062 m_ownTarget = new QAction(i18n("Add custom target"), m_targetActionGroup); 0063 m_ownTarget->setCheckable(true); 0064 m_targetMenu->addAction(m_ownTarget); 0065 } 0066 0067 TextEntry::~TextEntry() 0068 { 0069 m_targetMenu->deleteLater(); 0070 } 0071 0072 void TextEntry::populateMenu(QMenu* menu, QPointF pos) 0073 { 0074 if (m_rawCell) 0075 { 0076 menu->addAction(i18n("Convert to Text Entry"), this, &TextEntry::convertToTextEntry); 0077 menu->addMenu(m_targetMenu); 0078 } 0079 else 0080 { 0081 menu->addAction(i18n("Convert to Raw Cell"), this, &TextEntry::convertToRawCell); 0082 0083 bool imageSelected = false; 0084 QTextCursor cursor = m_textItem->textCursor(); 0085 const QChar repl = QChar::ObjectReplacementCharacter; 0086 if (cursor.hasSelection()) 0087 { 0088 QString selection = m_textItem->textCursor().selectedText(); 0089 imageSelected = selection.contains(repl); 0090 } 0091 else 0092 { 0093 // we need to try both the current cursor and the one after the that 0094 cursor = m_textItem->cursorForPosition(pos); 0095 for (int i = 2; i; --i) 0096 { 0097 int p = cursor.position(); 0098 if (m_textItem->document()->characterAt(p-1) == repl && 0099 cursor.charFormat().hasProperty(Cantor::Renderer::CantorFormula)) 0100 { 0101 m_textItem->setTextCursor(cursor); 0102 imageSelected = true; 0103 break; 0104 } 0105 cursor.movePosition(QTextCursor::NextCharacter); 0106 } 0107 } 0108 0109 if (imageSelected) 0110 { 0111 menu->addAction(i18n("Show LaTeX code"), this, SLOT(resolveImagesAtCursor())); 0112 } 0113 } 0114 menu->addSeparator(); 0115 WorksheetEntry::populateMenu(menu, pos); 0116 } 0117 0118 bool TextEntry::isEmpty() 0119 { 0120 return m_textItem->document()->isEmpty(); 0121 } 0122 0123 int TextEntry::type() const 0124 { 0125 return Type; 0126 } 0127 0128 bool TextEntry::acceptRichText() 0129 { 0130 return true; 0131 } 0132 0133 bool TextEntry::focusEntry(int pos, qreal xCoord) 0134 { 0135 if (aboutToBeRemoved()) 0136 return false; 0137 m_textItem->setFocusAt(pos, xCoord); 0138 return true; 0139 } 0140 0141 void TextEntry::setContent(const QString& content) 0142 { 0143 m_textItem->setPlainText(content); 0144 } 0145 0146 void TextEntry::setContent(const QDomElement& content, const KZip& file) 0147 { 0148 Q_UNUSED(file); 0149 if(content.firstChildElement(QLatin1String("body")).isNull()) 0150 return; 0151 0152 if (content.hasAttribute(QLatin1String("convertTarget"))) 0153 { 0154 convertToRawCell(); 0155 m_convertTarget = content.attribute(QLatin1String("convertTarget")); 0156 0157 // Set current action status 0158 int idx = standartRawCellTargetMimes.indexOf(m_convertTarget); 0159 if (idx != -1) 0160 m_targetMenu->actions()[idx]->setChecked(true); 0161 else 0162 addNewTarget(m_convertTarget); 0163 } 0164 else 0165 convertToTextEntry(); 0166 0167 QDomDocument doc = QDomDocument(); 0168 QDomNode n = doc.importNode(content.firstChildElement(QLatin1String("body")), true); 0169 doc.appendChild(n); 0170 QString html = doc.toString(); 0171 m_textItem->setHtml(html); 0172 } 0173 0174 void TextEntry::setContentFromJupyter(const QJsonObject& cell) 0175 { 0176 if (Cantor::JupyterUtils::isRawCell(cell)) 0177 { 0178 convertToRawCell(); 0179 0180 const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(cell); 0181 QJsonValue format = metadata.value(QLatin1String("format")); 0182 // Also checks "raw_mimetype", because raw cell don't corresponds Jupyter Notebook specification 0183 // See https://github.com/jupyter/notebook/issues/4730 0184 if (format.isUndefined()) 0185 format = metadata.value(QLatin1String("raw_mimetype")); 0186 m_convertTarget = format.toString(QString()); 0187 0188 // Set current action status 0189 int idx = standartRawCellTargetMimes.indexOf(m_convertTarget); 0190 if (idx != -1) 0191 m_targetMenu->actions()[idx]->setChecked(true); 0192 else 0193 addNewTarget(m_convertTarget); 0194 0195 m_textItem->setPlainText(Cantor::JupyterUtils::getSource(cell)); 0196 0197 setJupyterMetadata(metadata); 0198 } 0199 else if (Cantor::JupyterUtils::isMarkdownCell(cell)) 0200 { 0201 convertToTextEntry(); 0202 0203 QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell); 0204 m_textItem->setHtml(cantorMetadata.value(QLatin1String("text_entry_content")).toString()); 0205 } 0206 } 0207 0208 QJsonValue TextEntry::toJupyterJson() 0209 { 0210 // Simple logic: 0211 // If convertTarget is empty, it's user maded cell and we convert it to a markdown 0212 // If convertTarget set, it's raw cell from Jupyter and we convert it to Jupyter cell 0213 0214 QTextDocument* doc = m_textItem->document()->clone(); 0215 QTextCursor cursor = doc->find(QString(QChar::ObjectReplacementCharacter)); 0216 while(!cursor.isNull()) 0217 { 0218 QTextCharFormat format = cursor.charFormat(); 0219 if (format.hasProperty(Cantor::Renderer::CantorFormula)) 0220 { 0221 showLatexCode(cursor); 0222 } 0223 0224 cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); 0225 } 0226 0227 QJsonObject metadata(jupyterMetadata()); 0228 0229 QString entryData; 0230 QString entryType; 0231 0232 if (!m_rawCell) 0233 { 0234 entryType = QLatin1String("markdown"); 0235 0236 // Add raw text of entry to metadata, for situation when 0237 // Cantor opens .ipynb converted from our .cws format 0238 QJsonObject cantorMetadata; 0239 0240 if (Settings::storeTextEntryFormatting()) 0241 { 0242 entryData = doc->toHtml(); 0243 0244 // Remove DOCTYPE from html 0245 entryData.remove(QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>\\n"))); 0246 0247 cantorMetadata.insert(QLatin1String("text_entry_content"), entryData); 0248 } 0249 else 0250 entryData = doc->toPlainText(); 0251 0252 metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata); 0253 0254 // Replace our $$ formulas to $ 0255 entryData.replace(QLatin1String("$$"), QLatin1String("$")); 0256 } 0257 else 0258 { 0259 entryType = QLatin1String("raw"); 0260 metadata.insert(QLatin1String("format"), m_convertTarget); 0261 entryData = doc->toPlainText(); 0262 } 0263 0264 QJsonObject entry; 0265 entry.insert(QLatin1String("cell_type"), entryType); 0266 entry.insert(QLatin1String("metadata"), metadata); 0267 Cantor::JupyterUtils::setSource(entry, entryData); 0268 0269 return entry; 0270 } 0271 0272 QDomElement TextEntry::toXml(QDomDocument& doc, KZip* archive) 0273 { 0274 Q_UNUSED(archive); 0275 0276 QScopedPointer<QTextDocument> document(m_textItem->document()->clone()); 0277 0278 //make sure that the latex code is shown instead of the rendered formulas 0279 QTextCursor cursor = document->find(QString(QChar::ObjectReplacementCharacter)); 0280 while(!cursor.isNull()) 0281 { 0282 QTextCharFormat format = cursor.charFormat(); 0283 if (format.hasProperty(Cantor::Renderer::CantorFormula)) 0284 showLatexCode(cursor); 0285 0286 cursor = document->find(QString(QChar::ObjectReplacementCharacter), cursor); 0287 } 0288 0289 const QString& html = document->toHtml(); 0290 QDomElement el = doc.createElement(QLatin1String("Text")); 0291 QDomDocument myDoc = QDomDocument(); 0292 myDoc.setContent(html); 0293 el.appendChild(myDoc.documentElement().firstChildElement(QLatin1String("body"))); 0294 0295 if (m_rawCell) 0296 el.setAttribute(QLatin1String("convertTarget"), m_convertTarget); 0297 0298 return el; 0299 } 0300 0301 QString TextEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) 0302 { 0303 Q_UNUSED(commandSep); 0304 0305 if (commentStartingSeq.isEmpty()) 0306 return QString(); 0307 /* 0308 // would this be plain enough? 0309 QTextCursor cursor = m_textItem->textCursor(); 0310 cursor.movePosition(QTextCursor::Start); 0311 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 0312 0313 QString text = m_textItem->resolveImages(cursor); 0314 text.replace(QChar::ParagraphSeparator, '\n'); 0315 text.replace(QChar::LineSeparator, '\n'); 0316 */ 0317 QString text = m_textItem->toPlainText(); 0318 if (!commentEndingSeq.isEmpty()) 0319 return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n"); 0320 return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n"); 0321 0322 } 0323 0324 bool TextEntry::evaluate(EvaluationOption evalOp) 0325 { 0326 int i = 0; 0327 if (worksheet()->embeddedMathEnabled() && !m_rawCell) 0328 { 0329 // Render math in $$...$$ via Latex 0330 QTextCursor cursor = findLatexCode(); 0331 while (!cursor.isNull()) 0332 { 0333 QString latexCode = cursor.selectedText(); 0334 qDebug()<<"found latex: " << latexCode; 0335 0336 latexCode.remove(0, 2); 0337 latexCode.remove(latexCode.length() - 2, 2); 0338 latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); 0339 latexCode.replace(QChar::LineSeparator, QLatin1Char('\n')); 0340 0341 MathRenderer* renderer = worksheet()->mathRenderer(); 0342 renderer->renderExpression(++i, latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>))); 0343 0344 cursor = findLatexCode(cursor); 0345 } 0346 } 0347 0348 evaluateNext(evalOp); 0349 0350 return true; 0351 } 0352 0353 void TextEntry::updateEntry() 0354 { 0355 qDebug() << "update Entry"; 0356 QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); 0357 while(!cursor.isNull()) 0358 { 0359 QTextImageFormat format=cursor.charFormat().toImageFormat(); 0360 0361 if (format.hasProperty(Cantor::Renderer::CantorFormula)) 0362 worksheet()->mathRenderer()->rerender(m_textItem->document(), format); 0363 0364 cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); 0365 } 0366 } 0367 0368 void TextEntry::resolveImagesAtCursor() 0369 { 0370 QTextCursor cursor = m_textItem->textCursor(); 0371 if (!cursor.hasSelection()) 0372 cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); 0373 cursor.insertText(m_textItem->resolveImages(cursor)); 0374 } 0375 0376 QTextCursor TextEntry::findLatexCode(const QTextCursor& cursor) const 0377 { 0378 QTextDocument *doc = m_textItem->document(); 0379 QTextCursor startCursor; 0380 if (cursor.isNull()) 0381 startCursor = doc->find(QLatin1String("$$")); 0382 else 0383 startCursor = doc->find(QLatin1String("$$"), cursor); 0384 if (startCursor.isNull()) 0385 return startCursor; 0386 const QTextCursor endCursor = doc->find(QLatin1String("$$"), startCursor); 0387 if (endCursor.isNull()) 0388 return endCursor; 0389 startCursor.setPosition(startCursor.selectionStart()); 0390 startCursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor); 0391 return startCursor; 0392 } 0393 0394 QString TextEntry::showLatexCode(QTextCursor& cursor) 0395 { 0396 QString latexCode = cursor.charFormat().property(Cantor::Renderer::Code).toString(); 0397 cursor.deletePreviousChar(); 0398 latexCode = QLatin1String("$$") + latexCode + QLatin1String("$$"); 0399 cursor.insertText(latexCode); 0400 return latexCode; 0401 } 0402 0403 int TextEntry::searchText(const QString& text, const QString& pattern, 0404 QTextDocument::FindFlags qt_flags) 0405 { 0406 Qt::CaseSensitivity caseSensitivity; 0407 if (qt_flags & QTextDocument::FindCaseSensitively) 0408 caseSensitivity = Qt::CaseSensitive; 0409 else 0410 caseSensitivity = Qt::CaseInsensitive; 0411 0412 int position; 0413 if (qt_flags & QTextDocument::FindBackward) 0414 position = text.lastIndexOf(pattern, -1, caseSensitivity); 0415 else 0416 position = text.indexOf(pattern, 0, caseSensitivity); 0417 0418 return position; 0419 } 0420 0421 WorksheetCursor TextEntry::search(const QString& pattern, unsigned flags, 0422 QTextDocument::FindFlags qt_flags, 0423 const WorksheetCursor& pos) 0424 { 0425 if (!(flags & WorksheetEntry::SearchText) || 0426 (pos.isValid() && pos.entry() != this)) 0427 return WorksheetCursor(); 0428 0429 QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos); 0430 int position = 0; 0431 QTextCursor latexCursor; 0432 QString latex; 0433 if (flags & WorksheetEntry::SearchLaTeX) { 0434 const QString repl = QString(QChar::ObjectReplacementCharacter); 0435 latexCursor = m_textItem->search(repl, qt_flags, pos); 0436 while (!latexCursor.isNull()) { 0437 latex = m_textItem->resolveImages(latexCursor); 0438 position = searchText(latex, pattern, qt_flags); 0439 if (position >= 0) { 0440 break; 0441 } 0442 WorksheetCursor c(this, m_textItem, latexCursor); 0443 latexCursor = m_textItem->search(repl, qt_flags, c); 0444 } 0445 } 0446 0447 if (latexCursor.isNull()) { 0448 if (textCursor.isNull()) 0449 return WorksheetCursor(); 0450 else 0451 return WorksheetCursor(this, m_textItem, textCursor); 0452 } else { 0453 if (textCursor.isNull() || latexCursor < textCursor) { 0454 int start = latexCursor.selectionStart(); 0455 latexCursor.insertText(latex); 0456 QTextCursor c = m_textItem->textCursor(); 0457 c.setPosition(start + position); 0458 c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 0459 pattern.length()); 0460 return WorksheetCursor(this, m_textItem, c); 0461 } else { 0462 return WorksheetCursor(this, m_textItem, textCursor); 0463 } 0464 } 0465 } 0466 0467 0468 void TextEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force) 0469 { 0470 if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force) 0471 return; 0472 0473 const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin; 0474 0475 m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x); 0476 setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, m_textItem->height() + VerticalMargin)); 0477 } 0478 0479 bool TextEntry::wantToEvaluate() 0480 { 0481 return !findLatexCode().isNull(); 0482 } 0483 0484 bool TextEntry::isConvertableToTextEntry(const QJsonObject& cell) 0485 { 0486 if (!Cantor::JupyterUtils::isMarkdownCell(cell)) 0487 return false; 0488 0489 QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell); 0490 const QJsonValue& textContentValue = cantorMetadata.value(QLatin1String("text_entry_content")); 0491 0492 if (!textContentValue.isString()) 0493 return false; 0494 0495 const QString& textContent = textContentValue.toString(); 0496 const QString& source = Cantor::JupyterUtils::getSource(cell); 0497 0498 return textContent == source; 0499 } 0500 0501 void TextEntry::handleMathRender(QSharedPointer<MathRenderResult> result) 0502 { 0503 if (!result->successful) 0504 { 0505 qDebug() << "TextEntry: math render failed with message" << result->errorMessage; 0506 return; 0507 } 0508 0509 const QString& code = result->renderedMath.property(Cantor::Renderer::Code).toString(); 0510 const QString& delimiter = QLatin1String("$$"); 0511 QTextCursor cursor = m_textItem->document()->find(delimiter + code + delimiter); 0512 if (!cursor.isNull()) 0513 { 0514 m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image)); 0515 result->renderedMath.setProperty(Cantor::Renderer::Delimiter, QLatin1String("$$")); 0516 cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath); 0517 } 0518 } 0519 0520 void TextEntry::convertToRawCell() 0521 { 0522 m_rawCell = true; 0523 m_targetMenu->actions().at(0)->setChecked(true); 0524 0525 KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View); 0526 m_textItem->setBackgroundColor(scheme.background(KColorScheme::AlternateBackground).color()); 0527 0528 // Resolve all latex inserts 0529 QTextCursor cursor(m_textItem->document()); 0530 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 0531 cursor.insertText(m_textItem->resolveImages(cursor)); 0532 } 0533 0534 void TextEntry::convertToTextEntry() 0535 { 0536 m_rawCell = false; 0537 m_convertTarget.clear(); 0538 0539 KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View); 0540 m_textItem->setBackgroundColor(scheme.background(KColorScheme::NormalBackground).color()); 0541 } 0542 0543 void TextEntry::convertTargetChanged(QAction* action) 0544 { 0545 int index = standartRawCellTargetNames.indexOf(action->text()); 0546 if (index != -1) 0547 { 0548 m_convertTarget = standartRawCellTargetMimes[index]; 0549 } 0550 else if (action == m_ownTarget) 0551 { 0552 bool ok; 0553 const QString& target = QInputDialog::getText(worksheet()->worksheetView(), i18n("Cantor"), i18n("Target MIME type:"), QLineEdit::Normal, QString(), &ok); 0554 if (ok && !target.isEmpty()) 0555 { 0556 addNewTarget(target); 0557 m_convertTarget = target; 0558 } 0559 } 0560 else 0561 { 0562 m_convertTarget = action->text(); 0563 } 0564 } 0565 0566 void TextEntry::addNewTarget(const QString& target) 0567 { 0568 QAction* action = new QAction(target, m_targetActionGroup); 0569 action->setCheckable(true); 0570 action->setChecked(true); 0571 m_targetMenu->insertAction(m_targetMenu->actions().last(), action); 0572 } 0573 0574 QString TextEntry::text() const 0575 { 0576 return m_textItem->toPlainText(); 0577 }