KTextAddons

richtextbrowser.cpp
1/*
2 SPDX-FileCopyrightText: 2023-2024 Laurent Montel <montel.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "richtextbrowser.h"
8
9#include "widgets/textmessageindicator.h"
10#include <KCursor>
11#include <KLocalizedString>
12#include <KMessageBox>
13#include <KStandardAction>
14#include <KStandardGuiItem>
15#include <QIcon>
16
17#include "config-textcustomeditor.h"
18#if HAVE_KTEXTADDONS_KIO_SUPPORT
19#include <KIO/KUriFilterSearchProviderActions>
20#endif
21#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
22#include <TextEditTextToSpeech/TextToSpeech>
23#endif
24
25#include <KColorScheme>
26#include <QApplication>
27#include <QClipboard>
28#include <QContextMenuEvent>
29#include <QMenu>
30#include <QScrollBar>
31#include <QTextBlock>
32#include <QTextCursor>
33#include <QTextDocumentFragment>
34
35using namespace TextCustomEditor;
36class Q_DECL_HIDDEN RichTextBrowser::RichTextBrowserPrivate
37{
38public:
39 RichTextBrowserPrivate(RichTextBrowser *qq)
40 : q(qq)
41 , textIndicator(new TextCustomEditor::TextMessageIndicator(q))
42#if HAVE_KTEXTADDONS_KIO_SUPPORT
43 , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q))
44#endif
45 {
46 supportFeatures |= RichTextBrowser::Search;
47 supportFeatures |= RichTextBrowser::TextToSpeech;
48#if HAVE_KTEXTADDONS_KIO_SUPPORT
49 supportFeatures |= RichTextBrowser::AllowWebShortcut;
50#endif
51
52 // Workaround QTextEdit behavior: if the cursor points right after the link
53 // and start typing, the char format is kept. If user wants to write normal
54 // text right after the link, the only way is to move cursor at the next character
55 // (say for "<a>text</a>more text" the character has to be before letter "o"!)
56 // It's impossible if the whole document ends with a link.
57 // The same happens when text starts with a link: it's impossible to write normal text before it.
58 QObject::connect(q, &RichTextBrowser::cursorPositionChanged, q, [this]() {
59 QTextCursor c = q->textCursor();
60 if (c.charFormat().isAnchor() && !c.hasSelection()) {
61 QTextCharFormat fmt;
62 // If we are at block start or end (and at anchor), we just set the "default" format
63 if (!c.atBlockEnd() && !c.atBlockStart() && !c.hasSelection()) {
64 QTextCursor probe = c;
65 // Otherwise, if the next character is not a link, we just grab it's format
66 probe.movePosition(QTextCursor::NextCharacter);
67 if (!probe.charFormat().isAnchor()) {
68 fmt = probe.charFormat();
69 }
70 }
71 c.setCharFormat(fmt);
72 q->setTextCursor(c);
73 }
74 });
75 }
76
77 ~RichTextBrowserPrivate()
78 {
79 }
80
81 RichTextBrowser *const q;
82 TextCustomEditor::TextMessageIndicator *const textIndicator;
83 QTextDocumentFragment originalDoc;
84#if HAVE_KTEXTADDONS_KIO_SUPPORT
85 KIO::KUriFilterSearchProviderActions *const webshortcutMenuManager;
86#endif
88 QColor mReadOnlyBackgroundColor;
89 int mInitialFontSize;
90 bool customPalette = false;
91};
92
93RichTextBrowser::RichTextBrowser(QWidget *parent)
94 : QTextBrowser(parent)
95 , d(new RichTextBrowserPrivate(this))
96{
97 setAcceptRichText(true);
98 KCursor::setAutoHideCursor(this, true, false);
99 d->mInitialFontSize = font().pointSize();
100 regenerateColorScheme();
101}
102
103RichTextBrowser::~RichTextBrowser() = default;
104
105void RichTextBrowser::regenerateColorScheme()
106{
107 d->mReadOnlyBackgroundColor = KColorScheme(QPalette::Disabled, KColorScheme::View).background().color();
108 updateReadOnlyColor();
109}
110
111void RichTextBrowser::setDefaultFontSize(int val)
112{
113 d->mInitialFontSize = val;
114 slotZoomReset();
115}
116
117void RichTextBrowser::slotDisplayMessageIndicator(const QString &message)
118{
119 d->textIndicator->display(message);
120}
121
122void RichTextBrowser::contextMenuEvent(QContextMenuEvent *event)
123{
124 QMenu *popup = mousePopupMenu(event->pos());
125 if (popup) {
126 popup->exec(event->globalPos());
127 delete popup;
128 }
129}
130
131QMenu *RichTextBrowser::mousePopupMenu(QPoint pos)
132{
134 if (popup) {
135 const bool emptyDocument = document()->isEmpty();
136 if (!isReadOnly()) {
137 const QList<QAction *> actionList = popup->actions();
138 enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };
139 QAction *separatorAction = nullptr;
140 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1;
141 if (idx < actionList.count()) {
142 separatorAction = actionList.at(idx);
143 }
144 if (separatorAction) {
145 QAction *clearAllAction = KStandardAction::clear(this, &RichTextBrowser::slotUndoableClear, popup);
146 if (emptyDocument) {
147 clearAllAction->setEnabled(false);
148 }
149 popup->insertAction(separatorAction, clearAllAction);
150 }
151 }
152 if (searchSupport()) {
153 popup->addSeparator();
154 QAction *findAction = KStandardAction::find(this, &RichTextBrowser::findText, popup);
155 popup->addAction(findAction);
156 if (emptyDocument) {
157 findAction->setEnabled(false);
158 }
159 } else {
160 popup->addSeparator();
161 }
162
163#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
164 if (!emptyDocument) {
165 QAction *speakAction = popup->addAction(i18n("Speak Text"));
166 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
167 connect(speakAction, &QAction::triggered, this, &RichTextBrowser::slotSpeakText);
168 }
169#endif
170#if HAVE_KTEXTADDONS_KIO_SUPPORT
171 if (webShortcutSupport() && textCursor().hasSelection()) {
172 popup->addSeparator();
173 const QString selectedText = textCursor().selectedText();
174 d->webshortcutMenuManager->setSelectedText(selectedText);
175 d->webshortcutMenuManager->addWebShortcutsToMenu(popup);
176 }
177#endif
178 addExtraMenuEntry(popup, pos);
179 return popup;
180 }
181 return nullptr;
182}
183
184void RichTextBrowser::slotSpeakText()
185{
186 QString text;
187 if (textCursor().hasSelection()) {
188 text = textCursor().selectedText();
189 } else {
190 text = toPlainText();
191 }
192 Q_EMIT say(text);
193}
194
195void RichTextBrowser::setWebShortcutSupport(bool b)
196{
197#if HAVE_KTEXTADDONS_KIO_SUPPORT
198 if (b) {
199 d->supportFeatures |= AllowWebShortcut;
200 } else {
201 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut);
202 }
203#else
204 Q_UNUSED(b);
205#endif
206}
207
208bool RichTextBrowser::webShortcutSupport() const
209{
210#if HAVE_KTEXTADDONS_KIO_SUPPORT
211 return d->supportFeatures & AllowWebShortcut;
212#else
213 return false;
214#endif
215}
216
217void RichTextBrowser::setSearchSupport(bool b)
218{
219 if (b) {
220 d->supportFeatures |= Search;
221 } else {
222 d->supportFeatures = (d->supportFeatures & ~Search);
223 }
224}
225
226bool RichTextBrowser::searchSupport() const
227{
228 return d->supportFeatures & Search;
229}
230
231void RichTextBrowser::setTextToSpeechSupport(bool b)
232{
233 if (b) {
234 d->supportFeatures |= TextToSpeech;
235 } else {
236 d->supportFeatures = (d->supportFeatures & ~TextToSpeech);
237 }
238}
239
240bool RichTextBrowser::textToSpeechSupport() const
241{
242 return d->supportFeatures & TextToSpeech;
243}
244
245void RichTextBrowser::addExtraMenuEntry(QMenu *menu, QPoint pos)
246{
247 Q_UNUSED(menu)
248 Q_UNUSED(pos)
249}
250
251void RichTextBrowser::slotUndoableClear()
252{
254 cursor.beginEditBlock();
255 cursor.movePosition(QTextCursor::Start);
257 cursor.removeSelectedText();
258 cursor.endEditBlock();
259}
260
261void RichTextBrowser::updateReadOnlyColor()
262{
263 if (isReadOnly()) {
264 QPalette p = palette();
265 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor);
266 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor);
267 setPalette(p);
268 }
269}
270
271static void richTextDeleteWord(QTextCursor cursor, QTextCursor::MoveOperation op)
272{
273 cursor.clearSelection();
275 cursor.removeSelectedText();
276}
277
278void RichTextBrowser::deleteWordBack()
279{
280 richTextDeleteWord(textCursor(), QTextCursor::PreviousWord);
281}
282
283void RichTextBrowser::deleteWordForward()
284{
285 richTextDeleteWord(textCursor(), QTextCursor::WordRight);
286}
287
288bool RichTextBrowser::event(QEvent *ev)
289{
290 if (ev->type() == QEvent::ShortcutOverride) {
291 auto e = static_cast<QKeyEvent *>(ev);
292 if (overrideShortcut(e)) {
293 e->accept();
294 return true;
295 }
296 } else if (ev->type() == QEvent::ApplicationPaletteChange) {
297 regenerateColorScheme();
298 }
299 return QTextEdit::event(ev);
300}
301
302void RichTextBrowser::wheelEvent(QWheelEvent *event)
303{
305 const int angleDeltaY{event->angleDelta().y()};
306 if (angleDeltaY > 0) {
307 zoomIn();
308 } else if (angleDeltaY < 0) {
309 zoomOut();
310 }
311 event->accept();
312 return;
313 }
315}
316
317bool RichTextBrowser::handleShortcut(QKeyEvent *event)
318{
319 const int key = event->key() | event->modifiers();
320
321 if (KStandardShortcut::copy().contains(key)) {
322 copy();
323 return true;
324 } else if (KStandardShortcut::paste().contains(key)) {
325 paste();
326 return true;
327 } else if (KStandardShortcut::cut().contains(key)) {
328 cut();
329 return true;
330 } else if (KStandardShortcut::undo().contains(key)) {
331 if (!isReadOnly()) {
332 undo();
333 }
334 return true;
335 } else if (KStandardShortcut::redo().contains(key)) {
336 if (!isReadOnly()) {
337 redo();
338 }
339 return true;
340 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
341 if (!isReadOnly()) {
342 deleteWordBack();
343 }
344 return true;
345 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
346 if (!isReadOnly()) {
347 deleteWordForward();
348 }
349 return true;
350 } else if (KStandardShortcut::backwardWord().contains(key)) {
352 cursor.movePosition(QTextCursor::PreviousWord);
354 return true;
355 } else if (KStandardShortcut::forwardWord().contains(key)) {
357 cursor.movePosition(QTextCursor::NextWord);
359 return true;
360 } else if (KStandardShortcut::next().contains(key)) {
362 bool moved = false;
363 qreal lastY = cursorRect(cursor).bottom();
364 qreal distance = 0;
365 do {
366 qreal y = cursorRect(cursor).bottom();
367 distance += qAbs(y - lastY);
368 lastY = y;
369 moved = cursor.movePosition(QTextCursor::Down);
370 } while (moved && distance < viewport()->height());
371
372 if (moved) {
373 cursor.movePosition(QTextCursor::Up);
375 }
377 return true;
378 } else if (KStandardShortcut::prior().contains(key)) {
380 bool moved = false;
381 qreal lastY = cursorRect(cursor).bottom();
382 qreal distance = 0;
383 do {
384 qreal y = cursorRect(cursor).bottom();
385 distance += qAbs(y - lastY);
386 lastY = y;
387 moved = cursor.movePosition(QTextCursor::Up);
388 } while (moved && distance < viewport()->height());
389
390 if (moved) {
391 cursor.movePosition(QTextCursor::Down);
393 }
395 return true;
396 } else if (KStandardShortcut::begin().contains(key)) {
398 cursor.movePosition(QTextCursor::Start);
400 return true;
401 } else if (KStandardShortcut::end().contains(key)) {
403 cursor.movePosition(QTextCursor::End);
405 return true;
406 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
408 cursor.movePosition(QTextCursor::StartOfLine);
410 return true;
411 } else if (KStandardShortcut::endOfLine().contains(key)) {
413 cursor.movePosition(QTextCursor::EndOfLine);
415 return true;
416 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
417 Q_EMIT findText();
418 return true;
419 } else if (KStandardShortcut::pasteSelection().contains(key)) {
421 if (!text.isEmpty()) {
422 insertPlainText(text); // TODO: check if this is html? (MiB)
423 }
424 return true;
425 } else if (event == QKeySequence::DeleteEndOfLine) {
427 QTextBlock block = cursor.block();
428 if (cursor.position() == block.position() + block.length() - 2) {
430 } else {
432 }
433 cursor.removeSelectedText();
435 return true;
436 }
437
438 return false;
439}
440
441bool RichTextBrowser::overrideShortcut(QKeyEvent *event)
442{
443 const int key = event->key() | event->modifiers();
444
445 if (KStandardShortcut::copy().contains(key)) {
446 return true;
447 } else if (KStandardShortcut::paste().contains(key)) {
448 return true;
449 } else if (KStandardShortcut::cut().contains(key)) {
450 return true;
451 } else if (KStandardShortcut::undo().contains(key)) {
452 return true;
453 } else if (KStandardShortcut::redo().contains(key)) {
454 return true;
455 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
456 return true;
457 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
458 return true;
459 } else if (KStandardShortcut::backwardWord().contains(key)) {
460 return true;
461 } else if (KStandardShortcut::forwardWord().contains(key)) {
462 return true;
463 } else if (KStandardShortcut::next().contains(key)) {
464 return true;
465 } else if (KStandardShortcut::prior().contains(key)) {
466 return true;
467 } else if (KStandardShortcut::begin().contains(key)) {
468 return true;
469 } else if (KStandardShortcut::end().contains(key)) {
470 return true;
471 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
472 return true;
473 } else if (KStandardShortcut::endOfLine().contains(key)) {
474 return true;
475 } else if (KStandardShortcut::pasteSelection().contains(key)) {
476 return true;
477 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
478 return true;
479 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) {
480 return true;
481 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit
482 return true;
483 } else if (event == QKeySequence::DeleteEndOfLine) {
484 return true;
485 }
486 return false;
487}
488
489void RichTextBrowser::keyPressEvent(QKeyEvent *event)
490{
491 const bool isControlClicked = event->modifiers() & Qt::ControlModifier;
492 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier;
493 if (handleShortcut(event)) {
494 event->accept();
495 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) {
496 moveLineUpDown(true);
497 event->accept();
498 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) {
499 moveLineUpDown(false);
500 event->accept();
501 } else if (event->key() == Qt::Key_Up && isControlClicked) {
502 moveCursorBeginUpDown(true);
503 event->accept();
504 } else if (event->key() == Qt::Key_Down && isControlClicked) {
505 moveCursorBeginUpDown(false);
506 event->accept();
507 } else {
509 }
510}
511
512int RichTextBrowser::zoomFactor() const
513{
514 int pourcentage = 100;
515 const QFont f = font();
516 if (d->mInitialFontSize != f.pointSize()) {
517 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize;
518 }
519 return pourcentage;
520}
521
522void RichTextBrowser::slotZoomReset()
523{
524 QFont f = font();
525 if (d->mInitialFontSize != f.pointSize()) {
526 f.setPointSize(d->mInitialFontSize);
527 setFont(f);
528 }
529}
530
531void RichTextBrowser::moveCursorBeginUpDown(bool moveUp)
532{
536 cursor.clearSelection();
537 move.movePosition(QTextCursor::StartOfBlock);
538 move.movePosition(moveUp ? QTextCursor::PreviousBlock : QTextCursor::NextBlock);
539 move.endEditBlock();
541}
542
543void RichTextBrowser::moveLineUpDown(bool moveUp)
544{
548
549 const bool hasSelection = cursor.hasSelection();
550
551 if (hasSelection) {
552 move.setPosition(cursor.selectionStart());
553 move.movePosition(QTextCursor::StartOfBlock);
554 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
555 move.movePosition(move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
556 } else {
557 move.movePosition(QTextCursor::StartOfBlock);
559 }
560 const QString text = move.selectedText();
561
563 move.removeSelectedText();
564
565 if (moveUp) {
566 move.movePosition(QTextCursor::PreviousBlock);
567 move.insertBlock();
568 move.movePosition(QTextCursor::Left);
569 } else {
570 move.movePosition(QTextCursor::EndOfBlock);
571 if (move.atBlockStart()) { // empty block
572 move.movePosition(QTextCursor::NextBlock);
573 move.insertBlock();
574 move.movePosition(QTextCursor::Left);
575 } else {
576 move.insertBlock();
577 }
578 }
579
580 int start = move.position();
581 move.clearSelection();
582 move.insertText(text);
583 int end = move.position();
584
585 if (hasSelection) {
586 move.setPosition(end);
587 move.setPosition(start, QTextCursor::KeepAnchor);
588 } else {
589 move.setPosition(start);
590 }
591 move.endEditBlock();
592
594}
595
596#include "moc_richtextbrowser.cpp"
QBrush background(BackgroundRole=NormalBackground) const
static void setAutoHideCursor(QWidget *w, bool enable, bool customEventFilter=false)
The RichTextBrowser class.
A widget that displays messages in the top-left corner.
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QAction * find(const QObject *recvr, const char *slot, QObject *parent)
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
const QList< QKeySequence > & beginningOfLine()
const QList< QKeySequence > & begin()
const QList< QKeySequence > & cut()
const QList< QKeySequence > & undo()
const QList< QKeySequence > & next()
const QList< QKeySequence > & deleteWordBack()
const QList< QKeySequence > & find()
const QList< QKeySequence > & paste()
const QList< QKeySequence > & end()
const QList< QKeySequence > & copy()
const QList< QKeySequence > & backwardWord()
const QList< QKeySequence > & endOfLine()
const QList< QKeySequence > & forwardWord()
const QList< QKeySequence > & deleteWordForward()
const QList< QKeySequence > & findNext()
const QList< QKeySequence > & prior()
const QList< QKeySequence > & pasteSelection()
const QList< QKeySequence > & redo()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
virtual bool event(QEvent *event) override
QScrollBar * verticalScrollBar() const const
QWidget * viewport() const const
void triggerAction(SliderAction action)
void setEnabled(bool)
void setIcon(const QIcon &icon)
void triggered(bool checked)
const QColor & color() const const
QString text(Mode mode) const const
ShortcutOverride
void accept()
Type type() const const
int pointSize() const const
void setPointSize(int pointSize)
QClipboard * clipboard()
Qt::KeyboardModifiers keyboardModifiers()
QIcon fromTheme(const QString &name)
const_reference at(qsizetype i) const const
qsizetype count() const const
qsizetype indexOf(const AT &value, qsizetype from) const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
QAction * exec()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void setColor(ColorGroup group, ColorRole role, const QColor &color)
int bottom() const const
bool isEmpty() const const
ControlModifier
int length() const const
int position() const const
bool isAnchor() const const
void beginEditBlock()
QTextCharFormat charFormat() const const
void clearSelection()
bool hasSelection() const const
bool movePosition(MoveOperation operation, MoveMode mode, int n)
void removeSelectedText()
QString selectedText() const const
void setCharFormat(const QTextCharFormat &format)
void copy()
QMenu * createStandardContextMenu()
QRect cursorRect() const const
void cut()
void insertPlainText(const QString &text)
virtual void keyPressEvent(QKeyEvent *e) override
void paste()
bool isReadOnly() const const
void redo()
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
QString toPlainText() const const
void undo()
virtual void wheelEvent(QWheelEvent *e) override
void zoomIn(int range)
void zoomOut(int range)
QList< QAction * > actions() const const
void insertAction(QAction *before, QAction *action)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:51:28 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.