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{
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
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 }
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 }
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(i18nc("@action", "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(i18nc("@action", "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(i18nc("@action", "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{
424}
425
426void SieveTextEdit::setWordWrap(bool state)
427{
429}
430
431#include "moc_sievetextedit.cpp"
QString i18nc(const char *context, 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)
virtual bool event(QEvent *event) override
virtual void keyPressEvent(QKeyEvent *e) override
void setViewportMargins(const QMargins &margins)
QWidget * viewport() const const
QVariant data() const const
void triggered(bool checked)
ParagraphSeparator
ShortcutOverride
void accept()
void ignore()
Type type() const const
QFont systemFont(SystemFont type)
QRect boundingRect(QChar ch) const const
QIcon fromTheme(const QString &name)
int key() const const
const_reference at(qsizetype i) const const
QAction * addSeparator()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
T qobject_cast(QObject *object)
QObject * sender() const const
QRectF blockBoundingGeometry(const QTextBlock &block) const const
QRectF blockBoundingRect(const QTextBlock &block) const const
void blockCountChanged(int newBlockCount)
QPointF contentOffset() const const
QTextCursor cursorForPosition(const QPoint &pos) const const
QTextBlock firstVisibleBlock() const const
bool isReadOnly() const const
virtual void resizeEvent(QResizeEvent *e) override
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
void updateRequest(const QRect &rect, int dy)
int height() const const
int left() const const
int top() const const
int width() const const
qreal height() const const
qreal top() const const
QRectF translated(const QPointF &offset) 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
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
QRect contentsRect() const const
QFontMetrics fontMetrics() 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 Mon Nov 18 2024 12:14:30 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.