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"