File indexing completed on 2024-12-29 04:54:49
0001 /* SPDX-FileCopyrightText: 2011-2024 Laurent Montel <montel@kde.org> 0002 * 0003 * SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 0006 #include "sievetextedit.h" 0007 #include "editor/sieveeditorutil.h" 0008 #include "editor/sievelinenumberarea.h" 0009 #include "editor/sievetexteditorspellcheckdecorator.h" 0010 0011 #include <TextCustomEditor/PlainTextSyntaxSpellCheckingHighlighter> 0012 #include <TextCustomEditor/TextEditorCompleter> 0013 #include <TextUtils/ConvertText> 0014 0015 #include <KLocalizedString> 0016 #include <KSyntaxHighlighting/Definition> 0017 #include <KSyntaxHighlighting/Repository> 0018 #include <KSyntaxHighlighting/Theme> 0019 0020 #include <QAbstractItemView> 0021 #include <QAction> 0022 #include <QCompleter> 0023 #include <QFontDatabase> 0024 #include <QIcon> 0025 #include <QKeyEvent> 0026 #include <QMenu> 0027 #include <QPainter> 0028 #include <QTextDocumentFragment> 0029 using namespace KSieveUi; 0030 0031 class KSieveUi::SieveTextEditPrivate 0032 { 0033 public: 0034 SieveTextEditPrivate() = default; 0035 0036 SieveLineNumberArea *m_sieveLineNumberArea = nullptr; 0037 TextCustomEditor::TextEditorCompleter *mTextEditorCompleter = nullptr; 0038 KSyntaxHighlighting::Repository mSyntaxRepo; 0039 bool mShowHelpMenu = true; 0040 }; 0041 0042 SieveTextEdit::SieveTextEdit(QWidget *parent) 0043 : TextCustomEditor::PlainTextEditor(parent) 0044 , d(new KSieveUi::SieveTextEditPrivate) 0045 { 0046 setSpellCheckingConfigFileName(QStringLiteral("sieveeditorrc")); 0047 setWordWrapMode(QTextOption::NoWrap); 0048 setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 0049 d->m_sieveLineNumberArea = new SieveLineNumberArea(this); 0050 0051 connect(this, &SieveTextEdit::blockCountChanged, this, &SieveTextEdit::slotUpdateLineNumberAreaWidth); 0052 connect(this, &SieveTextEdit::updateRequest, this, &SieveTextEdit::slotUpdateLineNumberArea); 0053 0054 slotUpdateLineNumberAreaWidth(0); 0055 0056 initCompleter(); 0057 createHighlighter(); 0058 } 0059 0060 SieveTextEdit::~SieveTextEdit() 0061 { 0062 // disconnect these manually as the destruction of KPIMTextEdit::PlainTextEditorPrivate will trigger them 0063 disconnect(this, &SieveTextEdit::blockCountChanged, this, &SieveTextEdit::slotUpdateLineNumberAreaWidth); 0064 disconnect(this, &SieveTextEdit::updateRequest, this, &SieveTextEdit::slotUpdateLineNumberArea); 0065 } 0066 0067 void SieveTextEdit::updateHighLighter() 0068 { 0069 auto hlighter = dynamic_cast<TextCustomEditor::PlainTextSyntaxSpellCheckingHighlighter *>(highlighter()); 0070 if (hlighter) { 0071 hlighter->toggleSpellHighlighting(checkSpellingEnabled()); 0072 } 0073 } 0074 0075 void SieveTextEdit::clearDecorator() 0076 { 0077 // Nothing 0078 } 0079 0080 void SieveTextEdit::createHighlighter() 0081 { 0082 auto highlighter = new TextCustomEditor::PlainTextSyntaxSpellCheckingHighlighter(this); 0083 highlighter->toggleSpellHighlighting(checkSpellingEnabled()); 0084 highlighter->setCurrentLanguage(spellCheckingLanguage()); 0085 highlighter->setDefinition(d->mSyntaxRepo.definitionForName(QStringLiteral("Sieve"))); 0086 highlighter->setTheme((palette().color(QPalette::Base).lightness() < 128) ? d->mSyntaxRepo.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme) 0087 : d->mSyntaxRepo.defaultTheme(KSyntaxHighlighting::Repository::LightTheme)); 0088 setHighlighter(highlighter); 0089 } 0090 0091 void SieveTextEdit::resizeEvent(QResizeEvent *e) 0092 { 0093 QPlainTextEdit::resizeEvent(e); 0094 0095 const QRect cr = contentsRect(); 0096 d->m_sieveLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); 0097 } 0098 0099 int SieveTextEdit::lineNumberAreaWidth() const 0100 { 0101 int digits = 1; 0102 int max = qMax(1, blockCount()); 0103 while (max >= 10) { 0104 max /= 10; 0105 ++digits; 0106 } 0107 0108 const int space = 2 + fontMetrics().boundingRect(QLatin1Char('X')).width() * digits; 0109 return space; 0110 } 0111 0112 void SieveTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event) 0113 { 0114 QPainter painter(d->m_sieveLineNumberArea); 0115 painter.fillRect(event->rect(), Qt::lightGray); 0116 0117 QTextBlock block = firstVisibleBlock(); 0118 int blockNumber = block.blockNumber(); 0119 int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top()); 0120 int bottom = top + static_cast<int>(blockBoundingRect(block).height()); 0121 while (block.isValid() && top <= event->rect().bottom()) { 0122 if (block.isVisible() && bottom >= event->rect().top()) { 0123 const QString number = QString::number(blockNumber + 1); 0124 painter.setPen(Qt::black); 0125 painter.drawText(0, top, d->m_sieveLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); 0126 } 0127 0128 block = block.next(); 0129 top = bottom; 0130 bottom = top + static_cast<int>(blockBoundingRect(block).height()); 0131 ++blockNumber; 0132 } 0133 } 0134 0135 void SieveTextEdit::slotUpdateLineNumberAreaWidth(int) 0136 { 0137 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); 0138 } 0139 0140 void SieveTextEdit::slotUpdateLineNumberArea(const QRect &rect, int dy) 0141 { 0142 if (dy) { 0143 d->m_sieveLineNumberArea->scroll(0, dy); 0144 } else { 0145 d->m_sieveLineNumberArea->update(0, rect.y(), d->m_sieveLineNumberArea->width(), rect.height()); 0146 } 0147 0148 if (rect.contains(viewport()->rect())) { 0149 slotUpdateLineNumberAreaWidth(0); 0150 } 0151 } 0152 0153 QStringList SieveTextEdit::completerList() const 0154 { 0155 QStringList listWord; 0156 0157 listWord << QStringLiteral("require") << QStringLiteral("stop"); 0158 listWord << QStringLiteral(":contains") << QStringLiteral(":matches") << QStringLiteral(":is") << QStringLiteral(":over") << QStringLiteral(":under") 0159 << QStringLiteral(":all") << QStringLiteral(":domain") << QStringLiteral(":localpart"); 0160 listWord << QStringLiteral("if") << QStringLiteral("elsif") << QStringLiteral("else"); 0161 listWord << QStringLiteral("keep") << QStringLiteral("reject") << QStringLiteral("discard") << QStringLiteral("redirect") << QStringLiteral("addflag") 0162 << QStringLiteral("setflag"); 0163 listWord << QStringLiteral("address") << QStringLiteral("allof") << QStringLiteral("anyof") << QStringLiteral("exists") << QStringLiteral("false") 0164 << QStringLiteral("header") << QStringLiteral("not") << QStringLiteral("size") << QStringLiteral("true"); 0165 listWord << QStringLiteral(":days") << QStringLiteral(":seconds") << QStringLiteral(":subject") << QStringLiteral(":addresses") << QStringLiteral(":text"); 0166 listWord << QStringLiteral(":name") << QStringLiteral(":headers") << QStringLiteral(":first") << QStringLiteral(":importance"); 0167 listWord << QStringLiteral(":message") << QStringLiteral(":from"); 0168 0169 return listWord; 0170 } 0171 0172 void SieveTextEdit::setCompleterList(const QStringList &list) 0173 { 0174 d->mTextEditorCompleter->setCompleterStringList(list); 0175 } 0176 0177 void SieveTextEdit::initCompleter() 0178 { 0179 const QStringList listWord = completerList(); 0180 0181 d->mTextEditorCompleter = new TextCustomEditor::TextEditorCompleter(this, this); 0182 d->mTextEditorCompleter->setCompleterStringList(listWord); 0183 } 0184 0185 bool SieveTextEdit::event(QEvent *ev) 0186 { 0187 if (ev->type() == QEvent::ShortcutOverride) { 0188 auto e = static_cast<QKeyEvent *>(ev); 0189 if (overrideShortcut(e)) { 0190 e->accept(); 0191 return true; 0192 } 0193 } 0194 return TextCustomEditor::PlainTextEditor::event(ev); 0195 } 0196 0197 Sonnet::SpellCheckDecorator *SieveTextEdit::createSpellCheckDecorator() 0198 { 0199 return new SieveTextEditorSpellCheckDecorator(this); 0200 } 0201 0202 bool SieveTextEdit::overrideShortcut(QKeyEvent *event) 0203 { 0204 if (event->key() == Qt::Key_F1) { 0205 if (openVariableHelp()) { 0206 return true; 0207 } 0208 } 0209 return PlainTextEditor::overrideShortcut(event); 0210 } 0211 0212 bool SieveTextEdit::openVariableHelp() 0213 { 0214 if (!textCursor().hasSelection()) { 0215 const QString word = selectedWord(); 0216 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word); 0217 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) { 0218 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type); 0219 if (!url.isEmpty()) { 0220 return true; 0221 } 0222 } 0223 } 0224 return false; 0225 } 0226 0227 void SieveTextEdit::keyPressEvent(QKeyEvent *e) 0228 { 0229 if (d->mTextEditorCompleter->completer()->popup()->isVisible()) { 0230 switch (e->key()) { 0231 case Qt::Key_Enter: 0232 case Qt::Key_Return: 0233 case Qt::Key_Escape: 0234 case Qt::Key_Tab: 0235 case Qt::Key_Backtab: 0236 e->ignore(); 0237 return; // let the completer do default behavior 0238 default: 0239 break; 0240 } 0241 } else if (handleShortcut(e)) { 0242 return; 0243 } 0244 TextCustomEditor::PlainTextEditor::keyPressEvent(e); 0245 if (e->key() == Qt::Key_F1 && !textCursor().hasSelection()) { 0246 const QString word = selectedWord(); 0247 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word); 0248 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) { 0249 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type); 0250 if (!url.isEmpty()) { 0251 Q_EMIT openHelp(url); 0252 } 0253 } 0254 return; 0255 } 0256 d->mTextEditorCompleter->completeText(); 0257 } 0258 0259 void SieveTextEdit::setSieveCapabilities(const QStringList &capabilities) 0260 { 0261 setCompleterList(completerList() + capabilities); 0262 } 0263 0264 void SieveTextEdit::setShowHelpMenu(bool b) 0265 { 0266 d->mShowHelpMenu = b; 0267 } 0268 0269 void SieveTextEdit::addExtraMenuEntry(QMenu *menu, QPoint pos) 0270 { 0271 if (!d->mShowHelpMenu) { 0272 return; 0273 } 0274 0275 if (!textCursor().hasSelection()) { 0276 if (!isReadOnly()) { 0277 auto insertRules = new QAction(i18n("Insert Rule"), menu); 0278 // editRules->setIcon(QIcon::fromTheme(QStringLiteral("help-hint"))); 0279 connect(insertRules, &QAction::triggered, this, &SieveTextEdit::insertRule); 0280 QAction *act = menu->addSeparator(); 0281 menu->insertActions(menu->actions().at(0), {insertRules, act}); 0282 } 0283 0284 const QString word = selectedWord(pos); 0285 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word); 0286 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) { 0287 auto separator = new QAction(menu); 0288 separator->setSeparator(true); 0289 menu->insertAction(menu->actions().at(0), separator); 0290 0291 auto searchAction = new QAction(i18n("Help about: \'%1\'", word), menu); 0292 searchAction->setShortcut(Qt::Key_F1); 0293 searchAction->setIcon(QIcon::fromTheme(QStringLiteral("help-hint"))); 0294 searchAction->setData(word); 0295 connect(searchAction, &QAction::triggered, this, &SieveTextEdit::slotHelp); 0296 menu->insertAction(menu->actions().at(0), searchAction); 0297 } 0298 } else { 0299 if (!isReadOnly()) { 0300 auto editRules = new QAction(i18n("Edit Rule"), menu); 0301 // editRules->setIcon(QIcon::fromTheme(QStringLiteral("help-hint"))); 0302 connect(editRules, &QAction::triggered, this, &SieveTextEdit::slotEditRule); 0303 QAction *act = menu->addSeparator(); 0304 menu->insertActions(menu->actions().at(0), {editRules, act}); 0305 } 0306 } 0307 } 0308 0309 QString SieveTextEdit::selectedWord(QPoint pos) const 0310 { 0311 QTextCursor wordSelectCursor(pos.isNull() ? textCursor() : cursorForPosition(pos)); 0312 wordSelectCursor.clearSelection(); 0313 wordSelectCursor.select(QTextCursor::WordUnderCursor); 0314 const QString word = wordSelectCursor.selectedText(); 0315 return word; 0316 } 0317 0318 void SieveTextEdit::slotEditRule() 0319 { 0320 QTextCursor textcursor = textCursor(); 0321 Q_EMIT editRule(textcursor.selection().toPlainText()); 0322 } 0323 0324 void SieveTextEdit::slotHelp() 0325 { 0326 auto act = qobject_cast<QAction *>(sender()); 0327 if (act) { 0328 const QString word = act->data().toString(); 0329 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word); 0330 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type); 0331 if (!url.isEmpty()) { 0332 Q_EMIT openHelp(url); 0333 } 0334 } 0335 } 0336 0337 void SieveTextEdit::comment() 0338 { 0339 QTextCursor textcursor = textCursor(); 0340 if (textcursor.hasSelection()) { 0341 // Move start block 0342 textcursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); 0343 QString text = textcursor.selectedText(); 0344 text = QLatin1Char('#') + text; 0345 text.replace(QChar::ParagraphSeparator, QStringLiteral("\n#")); 0346 textcursor.insertText(text); 0347 setTextCursor(textcursor); 0348 } else { 0349 textcursor.movePosition(QTextCursor::StartOfBlock); 0350 textcursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0351 const QString s = textcursor.selectedText(); 0352 const QString str = QLatin1Char('#') + s; 0353 textcursor.insertText(str); 0354 setTextCursor(textcursor); 0355 } 0356 } 0357 0358 void SieveTextEdit::upperCase() 0359 { 0360 QTextCursor cursorText = textCursor(); 0361 TextUtils::ConvertText::upperCase(cursorText); 0362 } 0363 0364 void SieveTextEdit::lowerCase() 0365 { 0366 QTextCursor cursorText = textCursor(); 0367 TextUtils::ConvertText::lowerCase(cursorText); 0368 } 0369 0370 void SieveTextEdit::sentenceCase() 0371 { 0372 QTextCursor cursorText = textCursor(); 0373 TextUtils::ConvertText::sentenceCase(cursorText); 0374 } 0375 0376 void SieveTextEdit::reverseCase() 0377 { 0378 QTextCursor cursorText = textCursor(); 0379 TextUtils::ConvertText::reverseCase(cursorText); 0380 } 0381 0382 void SieveTextEdit::uncomment() 0383 { 0384 QTextCursor textcursor = textCursor(); 0385 if (textcursor.hasSelection()) { 0386 textcursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); 0387 QString text = textcursor.selectedText(); 0388 if (text.startsWith(QLatin1Char('#'))) { 0389 text.remove(0, 1); 0390 } 0391 QString newText = text; 0392 for (int i = 0; i < newText.length();) { 0393 if (newText.at(i) == QChar::ParagraphSeparator || newText.at(i) == QChar::LineSeparator) { 0394 ++i; 0395 if (i < newText.length()) { 0396 if (newText.at(i) == QLatin1Char('#')) { 0397 newText.remove(i, 1); 0398 } else { 0399 ++i; 0400 } 0401 } 0402 } else { 0403 ++i; 0404 } 0405 } 0406 0407 textcursor.insertText(newText); 0408 setTextCursor(textcursor); 0409 } else { 0410 textcursor.movePosition(QTextCursor::StartOfBlock); 0411 textcursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0412 QString text = textcursor.selectedText(); 0413 if (text.startsWith(QLatin1Char('#'))) { 0414 text.remove(0, 1); 0415 } 0416 textcursor.insertText(text); 0417 setTextCursor(textcursor); 0418 } 0419 } 0420 0421 bool SieveTextEdit::isWordWrap() const 0422 { 0423 return wordWrapMode() == QTextOption::WordWrap; 0424 } 0425 0426 void SieveTextEdit::setWordWrap(bool state) 0427 { 0428 setWordWrapMode(state ? QTextOption::WordWrap : QTextOption::NoWrap); 0429 } 0430 0431 #include "moc_sievetextedit.cpp"