Libksieve

sievetextedit.cpp
1/* SPDX-FileCopyrightText: 2011-2024 Laurent Montel <montel@kde.org>
2 *
3 * SPDX-License-Identifier: LGPL-2.0-or-later
4 */
5
6#include "sievetextedit.h"
7#include "editor/sieveeditorutil.h"
8#include "editor/sievelinenumberarea.h"
9#include "editor/sievetexteditorspellcheckdecorator.h"
10
11#include <TextCustomEditor/PlainTextSyntaxSpellCheckingHighlighter>
12#include <TextCustomEditor/TextEditorCompleter>
13#include <TextUtils/ConvertText>
14
15#include <KLocalizedString>
16#include <KSyntaxHighlighting/Definition>
17#include <KSyntaxHighlighting/Repository>
18#include <KSyntaxHighlighting/Theme>
19
20#include <QAbstractItemView>
21#include <QAction>
22#include <QCompleter>
23#include <QFontDatabase>
24#include <QIcon>
25#include <QKeyEvent>
26#include <QMenu>
27#include <QPainter>
28#include <QTextDocumentFragment>
29using namespace KSieveUi;
30
31class KSieveUi::SieveTextEditPrivate
32{
33public:
34 SieveTextEditPrivate() = default;
35
36 SieveLineNumberArea *m_sieveLineNumberArea = nullptr;
37 TextCustomEditor::TextEditorCompleter *mTextEditorCompleter = nullptr;
39 bool mShowHelpMenu = true;
40};
41
42SieveTextEdit::SieveTextEdit(QWidget *parent)
43 : TextCustomEditor::PlainTextEditor(parent)
44 , d(new KSieveUi::SieveTextEditPrivate)
45{
46 setSpellCheckingConfigFileName(QStringLiteral("sieveeditorrc"));
47 setWordWrapMode(QTextOption::NoWrap);
49 d->m_sieveLineNumberArea = new SieveLineNumberArea(this);
50
51 connect(this, &SieveTextEdit::blockCountChanged, this, &SieveTextEdit::slotUpdateLineNumberAreaWidth);
52 connect(this, &SieveTextEdit::updateRequest, this, &SieveTextEdit::slotUpdateLineNumberArea);
53
54 slotUpdateLineNumberAreaWidth(0);
55
56 initCompleter();
57 createHighlighter();
58}
59
60SieveTextEdit::~SieveTextEdit()
61{
62 // disconnect these manually as the destruction of KPIMTextEdit::PlainTextEditorPrivate will trigger them
63 disconnect(this, &SieveTextEdit::blockCountChanged, this, &SieveTextEdit::slotUpdateLineNumberAreaWidth);
64 disconnect(this, &SieveTextEdit::updateRequest, this, &SieveTextEdit::slotUpdateLineNumberArea);
65}
66
67void SieveTextEdit::updateHighLighter()
68{
69 auto hlighter = dynamic_cast<TextCustomEditor::PlainTextSyntaxSpellCheckingHighlighter *>(highlighter());
70 if (hlighter) {
71 hlighter->toggleSpellHighlighting(checkSpellingEnabled());
72 }
73}
74
75void SieveTextEdit::clearDecorator()
76{
77 // Nothing
78}
79
80void SieveTextEdit::createHighlighter()
81{
82 auto highlighter = new TextCustomEditor::PlainTextSyntaxSpellCheckingHighlighter(this);
83 highlighter->toggleSpellHighlighting(checkSpellingEnabled());
84 highlighter->setCurrentLanguage(spellCheckingLanguage());
85 highlighter->setDefinition(d->mSyntaxRepo.definitionForName(QStringLiteral("Sieve")));
86 highlighter->setTheme((palette().color(QPalette::Base).lightness() < 128) ? d->mSyntaxRepo.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme)
87 : d->mSyntaxRepo.defaultTheme(KSyntaxHighlighting::Repository::LightTheme));
88 setHighlighter(highlighter);
89}
90
91void SieveTextEdit::resizeEvent(QResizeEvent *e)
92{
94
95 const QRect cr = contentsRect();
96 d->m_sieveLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
97}
98
99int SieveTextEdit::lineNumberAreaWidth() const
100{
101 int digits = 1;
102 int max = qMax(1, blockCount());
103 while (max >= 10) {
104 max /= 10;
105 ++digits;
106 }
107
108 const int space = 2 + fontMetrics().boundingRect(QLatin1Char('X')).width() * digits;
109 return space;
110}
111
112void SieveTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
113{
114 QPainter painter(d->m_sieveLineNumberArea);
115 painter.fillRect(event->rect(), Qt::lightGray);
116
117 QTextBlock block = firstVisibleBlock();
118 int blockNumber = block.blockNumber();
119 int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
120 int bottom = top + static_cast<int>(blockBoundingRect(block).height());
121 while (block.isValid() && top <= event->rect().bottom()) {
122 if (block.isVisible() && bottom >= event->rect().top()) {
123 const QString number = QString::number(blockNumber + 1);
124 painter.setPen(Qt::black);
125 painter.drawText(0, top, d->m_sieveLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number);
126 }
127
128 block = block.next();
129 top = bottom;
130 bottom = top + static_cast<int>(blockBoundingRect(block).height());
131 ++blockNumber;
132 }
133}
134
135void SieveTextEdit::slotUpdateLineNumberAreaWidth(int)
136{
137 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
138}
139
140void SieveTextEdit::slotUpdateLineNumberArea(const QRect &rect, int dy)
141{
142 if (dy) {
143 d->m_sieveLineNumberArea->scroll(0, dy);
144 } else {
145 d->m_sieveLineNumberArea->update(0, rect.y(), d->m_sieveLineNumberArea->width(), rect.height());
146 }
147
148 if (rect.contains(viewport()->rect())) {
149 slotUpdateLineNumberAreaWidth(0);
150 }
151}
152
153QStringList SieveTextEdit::completerList() const
154{
155 QStringList listWord;
156
157 listWord << QStringLiteral("require") << QStringLiteral("stop");
158 listWord << QStringLiteral(":contains") << QStringLiteral(":matches") << QStringLiteral(":is") << QStringLiteral(":over") << QStringLiteral(":under")
159 << QStringLiteral(":all") << QStringLiteral(":domain") << QStringLiteral(":localpart");
160 listWord << QStringLiteral("if") << QStringLiteral("elsif") << QStringLiteral("else");
161 listWord << QStringLiteral("keep") << QStringLiteral("reject") << QStringLiteral("discard") << QStringLiteral("redirect") << QStringLiteral("addflag")
162 << QStringLiteral("setflag");
163 listWord << QStringLiteral("address") << QStringLiteral("allof") << QStringLiteral("anyof") << QStringLiteral("exists") << QStringLiteral("false")
164 << QStringLiteral("header") << QStringLiteral("not") << QStringLiteral("size") << QStringLiteral("true");
165 listWord << QStringLiteral(":days") << QStringLiteral(":seconds") << QStringLiteral(":subject") << QStringLiteral(":addresses") << QStringLiteral(":text");
166 listWord << QStringLiteral(":name") << QStringLiteral(":headers") << QStringLiteral(":first") << QStringLiteral(":importance");
167 listWord << QStringLiteral(":message") << QStringLiteral(":from");
168
169 return listWord;
170}
171
172void SieveTextEdit::setCompleterList(const QStringList &list)
173{
174 d->mTextEditorCompleter->setCompleterStringList(list);
175}
176
177void SieveTextEdit::initCompleter()
178{
179 const QStringList listWord = completerList();
180
181 d->mTextEditorCompleter = new TextCustomEditor::TextEditorCompleter(this, this);
182 d->mTextEditorCompleter->setCompleterStringList(listWord);
183}
184
185bool SieveTextEdit::event(QEvent *ev)
186{
187 if (ev->type() == QEvent::ShortcutOverride) {
188 auto e = static_cast<QKeyEvent *>(ev);
189 if (overrideShortcut(e)) {
190 e->accept();
191 return true;
192 }
193 }
194 return TextCustomEditor::PlainTextEditor::event(ev);
195}
196
197Sonnet::SpellCheckDecorator *SieveTextEdit::createSpellCheckDecorator()
198{
199 return new SieveTextEditorSpellCheckDecorator(this);
200}
201
202bool SieveTextEdit::overrideShortcut(QKeyEvent *event)
203{
204 if (event->key() == Qt::Key_F1) {
205 if (openVariableHelp()) {
206 return true;
207 }
208 }
209 return PlainTextEditor::overrideShortcut(event);
210}
211
212bool SieveTextEdit::openVariableHelp()
213{
214 if (!textCursor().hasSelection()) {
215 const QString word = selectedWord();
216 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word);
217 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) {
218 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type);
219 if (!url.isEmpty()) {
220 return true;
221 }
222 }
223 }
224 return false;
225}
226
227void SieveTextEdit::keyPressEvent(QKeyEvent *e)
228{
229 if (d->mTextEditorCompleter->completer()->popup()->isVisible()) {
230 switch (e->key()) {
231 case Qt::Key_Enter:
232 case Qt::Key_Return:
233 case Qt::Key_Escape:
234 case Qt::Key_Tab:
235 case Qt::Key_Backtab:
236 e->ignore();
237 return; // let the completer do default behavior
238 default:
239 break;
240 }
241 } else if (handleShortcut(e)) {
242 return;
243 }
244 TextCustomEditor::PlainTextEditor::keyPressEvent(e);
245 if (e->key() == Qt::Key_F1 && !textCursor().hasSelection()) {
246 const QString word = selectedWord();
247 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word);
248 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) {
249 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type);
250 if (!url.isEmpty()) {
251 Q_EMIT openHelp(url);
252 }
253 }
254 return;
255 }
256 d->mTextEditorCompleter->completeText();
257}
258
259void SieveTextEdit::setSieveCapabilities(const QStringList &capabilities)
260{
261 setCompleterList(completerList() + capabilities);
262}
263
264void SieveTextEdit::setShowHelpMenu(bool b)
265{
266 d->mShowHelpMenu = b;
267}
268
269void SieveTextEdit::addExtraMenuEntry(QMenu *menu, QPoint pos)
270{
271 if (!d->mShowHelpMenu) {
272 return;
273 }
274
275 if (!textCursor().hasSelection()) {
276 if (!isReadOnly()) {
277 auto insertRules = new QAction(i18n("Insert Rule"), menu);
278 // editRules->setIcon(QIcon::fromTheme(QStringLiteral("help-hint")));
279 connect(insertRules, &QAction::triggered, this, &SieveTextEdit::insertRule);
280 QAction *act = menu->addSeparator();
281 menu->insertActions(menu->actions().at(0), {insertRules, act});
282 }
283
284 const QString word = selectedWord(pos);
285 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word);
286 if (type != KSieveUi::SieveEditorUtil::UnknownHelp) {
287 auto separator = new QAction(menu);
288 separator->setSeparator(true);
289 menu->insertAction(menu->actions().at(0), separator);
290
291 auto searchAction = new QAction(i18n("Help about: \'%1\'", word), menu);
292 searchAction->setShortcut(Qt::Key_F1);
293 searchAction->setIcon(QIcon::fromTheme(QStringLiteral("help-hint")));
294 searchAction->setData(word);
295 connect(searchAction, &QAction::triggered, this, &SieveTextEdit::slotHelp);
296 menu->insertAction(menu->actions().at(0), searchAction);
297 }
298 } else {
299 if (!isReadOnly()) {
300 auto editRules = new QAction(i18n("Edit Rule"), menu);
301 // editRules->setIcon(QIcon::fromTheme(QStringLiteral("help-hint")));
302 connect(editRules, &QAction::triggered, this, &SieveTextEdit::slotEditRule);
303 QAction *act = menu->addSeparator();
304 menu->insertActions(menu->actions().at(0), {editRules, act});
305 }
306 }
307}
308
309QString SieveTextEdit::selectedWord(QPoint pos) const
310{
311 QTextCursor wordSelectCursor(pos.isNull() ? textCursor() : cursorForPosition(pos));
312 wordSelectCursor.clearSelection();
313 wordSelectCursor.select(QTextCursor::WordUnderCursor);
314 const QString word = wordSelectCursor.selectedText();
315 return word;
316}
317
318void SieveTextEdit::slotEditRule()
319{
320 QTextCursor textcursor = textCursor();
321 Q_EMIT editRule(textcursor.selection().toPlainText());
322}
323
324void SieveTextEdit::slotHelp()
325{
326 auto act = qobject_cast<QAction *>(sender());
327 if (act) {
328 const QString word = act->data().toString();
329 const KSieveUi::SieveEditorUtil::HelpVariableName type = KSieveUi::SieveEditorUtil::strToVariableName(word);
330 const QUrl url = KSieveUi::SieveEditorUtil::helpUrl(type);
331 if (!url.isEmpty()) {
332 Q_EMIT openHelp(url);
333 }
334 }
335}
336
337void SieveTextEdit::comment()
338{
339 QTextCursor textcursor = textCursor();
340 if (textcursor.hasSelection()) {
341 // Move start block
343 QString text = textcursor.selectedText();
344 text = QLatin1Char('#') + text;
345 text.replace(QChar::ParagraphSeparator, QStringLiteral("\n#"));
346 textcursor.insertText(text);
347 setTextCursor(textcursor);
348 } else {
351 const QString s = textcursor.selectedText();
352 const QString str = QLatin1Char('#') + s;
353 textcursor.insertText(str);
354 setTextCursor(textcursor);
355 }
356}
357
358void SieveTextEdit::upperCase()
359{
360 QTextCursor cursorText = textCursor();
361 TextUtils::ConvertText::upperCase(cursorText);
362}
363
364void SieveTextEdit::lowerCase()
365{
366 QTextCursor cursorText = textCursor();
367 TextUtils::ConvertText::lowerCase(cursorText);
368}
369
370void SieveTextEdit::sentenceCase()
371{
372 QTextCursor cursorText = textCursor();
373 TextUtils::ConvertText::sentenceCase(cursorText);
374}
375
376void SieveTextEdit::reverseCase()
377{
378 QTextCursor cursorText = textCursor();
379 TextUtils::ConvertText::reverseCase(cursorText);
380}
381
382void SieveTextEdit::uncomment()
383{
384 QTextCursor textcursor = textCursor();
385 if (textcursor.hasSelection()) {
387 QString text = textcursor.selectedText();
388 if (text.startsWith(QLatin1Char('#'))) {
389 text.remove(0, 1);
390 }
391 QString newText = text;
392 for (int i = 0; i < newText.length();) {
393 if (newText.at(i) == QChar::ParagraphSeparator || newText.at(i) == QChar::LineSeparator) {
394 ++i;
395 if (i < newText.length()) {
396 if (newText.at(i) == QLatin1Char('#')) {
397 newText.remove(i, 1);
398 } else {
399 ++i;
400 }
401 }
402 } else {
403 ++i;
404 }
405 }
406
407 textcursor.insertText(newText);
408 setTextCursor(textcursor);
409 } else {
412 QString text = textcursor.selectedText();
413 if (text.startsWith(QLatin1Char('#'))) {
414 text.remove(0, 1);
415 }
416 textcursor.insertText(text);
417 setTextCursor(textcursor);
418 }
419}
420
421bool SieveTextEdit::isWordWrap() const
422{
423 return wordWrapMode() == QTextOption::WordWrap;
424}
425
426void SieveTextEdit::setWordWrap(bool state)
427{
428 setWordWrapMode(state ? QTextOption::WordWrap : QTextOption::NoWrap);
429}
430
431#include "moc_sievetextedit.cpp"
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
Type type(const QSqlDatabase &db)
KIOCORE_EXPORT QString number(KIO::filesize_t size)
QVariant data() const const
void triggered(bool checked)
ParagraphSeparator
ShortcutOverride
void accept()
void ignore()
Type type() const const
QFont systemFont(SystemFont type)
QIcon fromTheme(const QString &name)
int key() const const
const_reference at(qsizetype i) const const
QAction * addSeparator()
virtual void resizeEvent(QResizeEvent *e) override
bool isNull() const const
bool contains(const QPoint &point, bool proper) const const
int height() const const
int left() const const
int top() const const
int y() const const
const QChar at(qsizetype position) const const
qsizetype length() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
AlignRight
lightGray
int blockNumber() const const
bool isValid() const const
bool isVisible() const const
QTextBlock next() const const
void clearSelection()
bool hasSelection() const const
void insertText(const QString &text)
bool movePosition(MoveOperation operation, MoveMode mode, int n)
QString selectedText() const const
QTextDocumentFragment selection() const const
QString toPlainText() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isEmpty() const const
QString toString() const const
QList< QAction * > actions() const const
void insertAction(QAction *before, QAction *action)
void insertActions(QAction *before, const QList< QAction * > &actions)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:19 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.