File indexing completed on 2022-11-29 18:16:32

0001 /*
0002     SPDX-FileCopyrightText: 2003-2008 Cies Breijs <cies AT kde DOT nl>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "editor.h"
0008 
0009 
0010 #include <QFileDialog>
0011 #include <QFontDatabase>
0012 #include <QSaveFile>
0013 #include <QTemporaryFile>
0014 #include <QTextStream>
0015 #include <QBuffer>
0016 #include <QHBoxLayout>
0017 
0018 #include <KFind>
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 #include <KIO/StoredTransferJob>
0022 
0023 static const int CURSOR_WIDTH = 2;  // in pixels
0024 static const int TAB_WIDTH    = 2;  // in character widths
0025 
0026 
0027 Editor::Editor(QWidget *parent)
0028     : QFrame(parent)
0029 {
0030     setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
0031     setLineWidth(CURSOR_WIDTH);
0032     setCurrentUrl();
0033     currentRow = 1;
0034     currentCol = 1;
0035 
0036     // setup the main view
0037     editor = new TextEdit(this);
0038     editor->document()->setDefaultFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0039     editor->setFrameStyle(QFrame::NoFrame);
0040     editor->installEventFilter(this);
0041     editor->setLineWrapMode(QTextEdit::WidgetWidth);
0042     editor->setTabStopDistance(editor->fontMetrics().boundingRect(QStringLiteral("0")).width() * TAB_WIDTH);
0043     editor->setAcceptRichText(false);
0044     setFocusProxy(editor);
0045     connect(editor->document(), &QTextDocument::contentsChange, this, &Editor::textChanged);
0046     connect(editor->document(), &QTextDocument::modificationChanged, this, &Editor::setModified);
0047     connect(editor, &TextEdit::cursorPositionChanged, this, &Editor::updateOnCursorPositionChange);
0048 
0049     // setup the line number pane
0050     numbers = new LineNumbers(this, editor);
0051     numbers->setFont(editor->document()->defaultFont());
0052     numbers->setWidth(1);
0053     connect(editor->document()->documentLayout(), SIGNAL(update(QRectF)), numbers, SLOT(update()));
0054     connect(editor->verticalScrollBar(), SIGNAL(valueChanged(int)), numbers, SLOT(update()));
0055 
0056     // let the line numbers and the editor coexist
0057     box = new QHBoxLayout(this);
0058     box->setSpacing(0);
0059     box->setContentsMargins(0, 0, 0, 0);
0060     box->addWidget(numbers);
0061     box->addWidget(editor);
0062 
0063     // calculate the bg color for the highlighted line
0064     QColor bgColor = this->palette().brush(this->backgroundRole()).color();
0065     highlightedLineBackgroundColor.setHsv(
0066         LINE_HIGHLIGHT_COLOR.hue(),
0067         bgColor.saturation() + EXTRA_SATURATION,
0068         bgColor.value());
0069 
0070     // our syntax highlighter (this does not do any markings)
0071     highlighter = new Highlighter(editor->document());
0072 
0073     // create a find dialog
0074     fdialog = new KFindDialog();
0075     fdialog->setSupportsRegularExpressionFind(false);
0076     fdialog->setHasSelection(false);
0077     fdialog->setHasCursor(false);
0078 
0079     // sets some more default values
0080     newFile();
0081     tokenizer = new Tokenizer();
0082 }
0083 
0084 Editor::~Editor()
0085 {
0086     delete highlighter;
0087     delete tokenizer;
0088 }
0089 
0090 void Editor::enable() {
0091     editor->viewport()->setEnabled(true);
0092     editor->setReadOnly(false);
0093 }
0094 
0095 void Editor::disable() {
0096     editor->viewport()->setEnabled(false);
0097     editor->setReadOnly(true);
0098 }
0099 
0100 
0101 
0102 void Editor::setContent(const QString& s)
0103 {
0104     editor->document()->setPlainText(s);
0105     editor->document()->setModified(false);
0106 }
0107 
0108 void Editor::openExample(const QString& example, const QString& exampleName)
0109 {
0110     if (newFile()) {
0111         setContent(example);
0112         editor->document()->setModified(false);
0113         setCurrentUrl(QUrl::fromLocalFile(exampleName));
0114     }
0115 }
0116 
0117 void Editor::textChanged(int pos, int removed, int added)
0118 {
0119     Q_UNUSED(pos);
0120     if (removed == 0 && added == 0) return;  // save some cpu cycles
0121     removeMarkings();  // removes the character markings if there are any
0122     int lineCount = 1;
0123     for (QTextBlock block = editor->document()->begin(); block.isValid(); block = block.next()) lineCount++;
0124     numbers->setWidth(qMax(1, 1 + static_cast<int>(std::floor(std::log10(static_cast<double>(lineCount) - 1)))));
0125 
0126     Q_EMIT contentChanged();
0127 }
0128 
0129 
0130 bool Editor::newFile()
0131 {
0132     if (maybeSave()) {
0133         editor->document()->clear();
0134         setCurrentUrl();
0135         return true;
0136     }
0137     return false;
0138 }
0139 
0140 bool Editor::openFile(const QUrl &_url)
0141 {
0142     QUrl url = _url;
0143     if (maybeSave()) {
0144         if (url.isEmpty()) {
0145             url = QFileDialog::getOpenFileUrl(this, 
0146                                               i18n("Open"), 
0147                                               QUrl(), 
0148                                               QStringLiteral("%1 (*.turtle);;%2 (*)").arg(i18n("Turtle code files")).arg(i18n("All files"))
0149                     );
0150         }
0151         if (!url.isEmpty()) {
0152             KIO::StoredTransferJob *job = KIO::storedGet(url);
0153             if (job->exec()) {
0154                 QByteArray data = job->data();
0155                 QBuffer buffer(&data);
0156                 if (!buffer.open(QIODevice::ReadOnly | QIODevice::Text)) {
0157                     return false; // can't happen
0158                 }
0159                 QTextStream in(&buffer);
0160                 // check for our magic identifier
0161                 QString s;
0162                 s = in.readLine();
0163                 if (s != KTURTLE_MAGIC_1_0) {
0164                     KMessageBox::error(this, i18n("The file you try to open is not a valid KTurtle script, or is incompatible with this version of KTurtle.\nCannot open %1", url.toDisplayString(QUrl::PreferLocalFile)));
0165                     return false;
0166                 }
0167                 QString localizedScript = Translator::instance()->localizeScript(in.readAll());
0168                 setContent(localizedScript);
0169                 setCurrentUrl(url);
0170                 editor->document()->setModified(false);
0171                 Q_EMIT fileOpened(url);
0172                 return true;
0173             } else {
0174                 KMessageBox::error(this, job->errorString());
0175                 return false;
0176             }
0177         }
0178     }
0179 //  statusbar "Nothing opened"
0180     return false;
0181 }
0182 
0183 bool Editor::saveFile(const QUrl &targetUrl)
0184 {
0185     QUrl url(targetUrl);
0186     bool result = false;
0187     if (url.isEmpty() && currentUrl().isEmpty()) {
0188         result = saveFileAs();
0189     } else {
0190         if (url.isEmpty()) url = currentUrl();
0191         QTemporaryFile tmp;  // only used for network export
0192         tmp.setAutoRemove(false);
0193         tmp.open();
0194         QString filename = url.isLocalFile() ? url.toLocalFile() : tmp.fileName();
0195     
0196         QSaveFile *savefile = new QSaveFile(filename);
0197         if (savefile->open(QIODevice::WriteOnly)) {
0198             QTextStream outputStream(savefile);
0199             // store commands in their generic @(...) notation format, to be translatable when reopened
0200             // this allows sharing of scripts written in different languages
0201             Tokenizer tokenizer;
0202             tokenizer.initialize(editor->document()->toPlainText());
0203             const QStringList localizedLooks(Translator::instance()->allLocalizedLooks());
0204             QString untranslated;
0205             Token* t;
0206             bool pendingEOL = false;  // to avoid writing a final EOL token
0207             while ((t = tokenizer.getToken())->type() != Token::EndOfInput) {
0208                 if (pendingEOL) {
0209                     untranslated.append(QLatin1Char('\n'));
0210                     pendingEOL = false;
0211                 }
0212                 if (localizedLooks.contains(t->look())) {
0213                     QString defaultLook(Translator::instance()->defaultLook(t->look()));
0214                     untranslated.append(QStringLiteral("@(%1)").arg(defaultLook));
0215                 } else {
0216                     if (t->type() == Token::EndOfLine) 
0217                         pendingEOL = true;
0218                     else
0219                         untranslated.append(t->look());
0220                 }
0221             }
0222             outputStream << KTURTLE_MAGIC_1_0 << '\n';
0223             outputStream << untranslated;
0224             outputStream.flush();
0225             if (savefile->commit()) {
0226                 result = true;
0227                 if (!url.isLocalFile()) {
0228                     KIO::StoredTransferJob *job = KIO::storedPut(savefile, url, -1, KIO::DefaultFlags);
0229                     if (!job->exec()) {
0230                         result = false;
0231                         KMessageBox::error(this, i18n("Could not save file."));
0232                     }
0233                 }
0234             } else {
0235                 // Error.
0236                 result = false;
0237                 KMessageBox::error(this, i18n("Could not save file."));
0238             }
0239             if (result) {
0240                 setCurrentUrl(url);
0241                 editor->document()->setModified(false);
0242                 // MainWindow will add us to the recent file list
0243                 Q_EMIT fileSaved(url);
0244             }
0245         }
0246         delete savefile;
0247     }
0248     return result;
0249 }
0250 
0251 bool Editor::saveFileAs()
0252 {
0253     QUrl url = QFileDialog::getSaveFileUrl(this,
0254                                            i18n("Save As"),
0255                                            QUrl(),
0256                                            QStringLiteral("%1 (*.turtle);;%2 (*)").arg(i18n("Turtle code files")).arg(i18n("All files"))
0257                                            );
0258     if (url.isEmpty()) return false;
0259     bool result = saveFile(url);
0260     return result;
0261 }
0262 
0263 bool Editor::maybeSave()
0264 {
0265     if (!editor->document()->isModified()) return true;
0266     int result = KMessageBox::warningContinueCancel(this,
0267         i18n("The program you are currently working on is not saved. "
0268              "By continuing you may lose the changes you have made."),
0269         i18n("Unsaved File"), KGuiItem(i18n("&Discard Changes")), KStandardGuiItem::cancel(), i18n("&Discard Changes"));
0270     if (result == KMessageBox::Continue) return true;
0271     return false;
0272 }
0273 
0274 
0275 void Editor::setModified(bool b)
0276 {
0277     editor->document()->setModified(b);
0278     Q_EMIT modificationChanged();
0279 }
0280 
0281 // TODO: improve find to be able to search within a selection
0282 void Editor::find()
0283 {
0284     // find selection, etc
0285     if (editor->textCursor().hasSelection()) {
0286         QString selectedText = editor->textCursor().selectedText();
0287         // If the selection is too big, then we don't want to automatically
0288         // populate the search text box with the selection text
0289         if (selectedText.length() < 30) {
0290             fdialog->setPattern(selectedText);
0291         }
0292     }
0293     if (fdialog->exec() == QDialog::Accepted && !fdialog->pattern().isEmpty()) {
0294         long kOpts = fdialog->options();
0295         QTextDocument::FindFlags qOpts = {};
0296         if (kOpts & KFind::CaseSensitive)  { qOpts |= QTextDocument::FindCaseSensitively; }
0297         if (kOpts & KFind::FindBackwards)  { qOpts |= QTextDocument::FindBackward; }
0298         if (kOpts & KFind::WholeWordsOnly) { qOpts |= QTextDocument::FindWholeWords; }
0299         editor->find(fdialog->pattern(), qOpts);
0300     }
0301 }
0302 
0303 void Editor::findNext()
0304 {
0305     if (!fdialog->pattern().isEmpty()) {
0306         long kOpts = fdialog->options();
0307         QTextDocument::FindFlags qOpts = {};
0308         if (kOpts & KFind::CaseSensitive)  { qOpts |= QTextDocument::FindCaseSensitively; }
0309         if (kOpts & KFind::FindBackwards)  { qOpts |= QTextDocument::FindBackward; }
0310         if (kOpts & KFind::WholeWordsOnly) { qOpts |= QTextDocument::FindWholeWords; }
0311         editor->find(fdialog->pattern(), qOpts);
0312     }
0313 }
0314 
0315 void Editor::findPrev()
0316 {
0317     if(!fdialog->pattern().isEmpty()) {
0318         long kOpts = fdialog->options();
0319         QTextDocument::FindFlags qOpts = {};
0320         if (kOpts & KFind::CaseSensitive)    { qOpts |= QTextDocument::FindCaseSensitively; }
0321         // search in the opposite direction as findNext()
0322         if (!(kOpts & KFind::FindBackwards)) { qOpts |= QTextDocument::FindBackward; }
0323         if (kOpts & KFind::WholeWordsOnly)   { qOpts |= QTextDocument::FindWholeWords; }
0324         editor->find(fdialog->pattern(), qOpts);
0325     }
0326 }
0327 
0328 void Editor::setCurrentUrl(const QUrl &url)
0329 {
0330     m_currentUrl = url;
0331     Q_EMIT contentNameChanged(m_currentUrl.fileName());
0332 }
0333 
0334 void Editor::setOverwriteMode(bool b)
0335 {
0336     editor->setOverwriteMode(b);
0337     editor->setCursorWidth(b ? editor->fontMetrics().boundingRect(QStringLiteral("0")).width() : 2);
0338 }
0339 
0340 
0341 void Editor::updateOnCursorPositionChange()
0342 {
0343     // convert the absolute pos into a row/col pair, and set current line aswell
0344     QString s = editor->toPlainText();
0345     int pos = editor->textCursor().position();
0346     int row = 1;
0347     int last_break = -1;
0348     int next_break = 0;
0349     for (int i = 0; i < s.length(); i++) {
0350         if (s.at(i) == QLatin1Char('\n') && i < pos) {
0351             last_break = i;
0352             row++;
0353         } else if (s.at(i) == QLatin1Char('\n') && i >= pos) {
0354             next_break = i;
0355             break;
0356         }
0357     }
0358     if (next_break == 0)
0359         next_break = s.length();
0360     if (currentRow != row) {
0361         currentRow = row;
0362         highlightCurrentLine();
0363         editor->highlightCurrentLine();
0364     }
0365     currentCol = pos - last_break;
0366     currentLine = s.mid(last_break+1, next_break-last_break-1);
0367     Q_EMIT cursorPositionChanged();
0368 }
0369 
0370 Token* Editor::currentToken()
0371 {
0372     tokenizer->initialize(currentLine);
0373     Token* token = tokenizer->getToken();
0374     while (token->type() != Token::EndOfInput) {
0375         if (currentCol >= token->startCol() && currentCol <= token->endCol())
0376             return token;
0377         delete token;
0378         token = tokenizer->getToken();
0379     }
0380     delete token;
0381     return nullptr;
0382 }
0383 
0384 
0385 void Editor::insertPlainText(const QString& txt)
0386 {
0387     editor->textCursor().insertText(txt);
0388 }
0389 
0390 
0391 void Editor::paintEvent(QPaintEvent *event)
0392 {
0393     QRect rect = editor->currentLineRect();
0394     rect.setWidth(this->width() - EDITOR_MARGIN);  // don't draw too much
0395     rect.translate(0, EDITOR_MARGIN);  // small hack to nicely align the line highlighting
0396     //QColor bgColor = this->palette().brush(this->backgroundRole()).color();
0397     QPainter painter(this);
0398     const QBrush brush(highlightedLineBackgroundColor);
0399     painter.fillRect(rect, brush);
0400     painter.end();
0401     QFrame::paintEvent(event);
0402 }
0403 
0404 QString Editor::toHtml(const QString& title, const QString& lang)
0405 {
0406     Tokenizer* tokenizer = new Tokenizer();
0407     tokenizer->initialize(editor->document()->toPlainText());
0408     QString html = QString();
0409     QTextCharFormat* format;
0410     Token* token = tokenizer->getToken();
0411     while (token->type() != Token::EndOfInput) {
0412         QString escaped;
0413         switch (token->type()) {
0414             case Token::EndOfLine:  escaped = QStringLiteral("<br />"); break;
0415             case Token::WhiteSpace: escaped = QLatin1String(""); for (int n = 0; n < token->look().length(); n++) { escaped += QLatin1String("&nbsp;"); } break;
0416             default:                escaped = token->look().toHtmlEscaped(); break;
0417         }
0418         format = highlighter->tokenToFormat(token);
0419         if (format) {
0420             bool bold = format->fontWeight() > 50;
0421             html += QStringLiteral("<span style=\"color: %1;%2\">%3</span>")
0422                 .arg(format->foreground().color().name())
0423                         .arg(bold ? QStringLiteral(" font-weight: bold;") : QString())
0424                 .arg(escaped);
0425         } else {
0426             html += escaped;
0427         }
0428         token = tokenizer->getToken();
0429     }
0430     delete tokenizer;
0431     return QStringLiteral("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"%1\" lang=\"%1\">"
0432                    "<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\" />"
0433                    "<title>%2</title></head>"
0434                    "<body style=\"font-family: monospace;\">%3</body></html>").arg(lang).arg(title).arg(html);
0435 }
0436 
0437 
0438 // bool Editor::eventFilter(QObject *obj, QEvent *event)
0439 // {
0440 //  if (obj != editor) return QFrame::eventFilter(obj, event);
0441 // 
0442 //  if (event->type() == QEvent::ToolTip) {
0443 //      QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
0444 // 
0445 //      QTextCursor cursor = editor->cursorForPosition(helpEvent->pos());
0446 //      cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
0447 //      cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0448 // 
0449 //      QString word = cursor.selectedText();
0450 //      Q_EMIT mouseHover(word);
0451 //      Q_EMIT mouseHover(helpEvent->pos(), word);
0452 // 
0453 //      // QToolTip::showText(helpEvent->globalPos(), word); // For testing
0454 //  }
0455 // 
0456 //  return false;
0457 // }