File indexing completed on 2022-09-27 12:31:09

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