File indexing completed on 2024-04-28 07:33:17
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(" "); } 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 // } 0458 0459 #include "moc_editor.cpp"