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()) {
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
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 {
139 UndoAct,
140 RedoAct,
141 CutAct,
142 CopyAct,
143 PasteAct,
144 ClearAct,
145 SelectAllAct,
146 NCountActs
147 };
148 QAction *separatorAction = nullptr;
149 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1;
150 if (idx < actionList.count()) {
151 separatorAction = actionList.at(idx);
152 }
153 if (separatorAction) {
154 QAction *clearAllAction = KStandardAction::clear(this, &RichTextBrowser::slotUndoableClear, popup);
155 if (emptyDocument) {
156 clearAllAction->setEnabled(false);
157 }
158 popup->insertAction(separatorAction, clearAllAction);
159 }
160 }
161 if (searchSupport()) {
162 popup->addSeparator();
163 QAction *findAction = KStandardAction::find(this, &RichTextBrowser::findText, popup);
164 popup->addAction(findAction);
165 if (emptyDocument) {
166 findAction->setEnabled(false);
167 }
168 } else {
169 popup->addSeparator();
170 }
171
172#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
173 if (!emptyDocument) {
174 QAction *speakAction = popup->addAction(i18n("Speak Text"));
175 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
176 connect(speakAction, &QAction::triggered, this, &RichTextBrowser::slotSpeakText);
177 }
178#endif
179#if HAVE_KTEXTADDONS_KIO_SUPPORT
180 if (webShortcutSupport() && textCursor().hasSelection()) {
181 popup->addSeparator();
182 const QString selectedText = textCursor().selectedText();
183 d->webshortcutMenuManager->setSelectedText(selectedText);
184 d->webshortcutMenuManager->addWebShortcutsToMenu(popup);
185 }
186#endif
187 addExtraMenuEntry(popup, pos);
188 return popup;
189 }
190 return nullptr;
191}
192
193void RichTextBrowser::slotSpeakText()
194{
195 QString text;
196 if (textCursor().hasSelection()) {
197 text = textCursor().selectedText();
198 } else {
199 text = toPlainText();
200 }
201 Q_EMIT say(text);
202}
203
204void RichTextBrowser::setWebShortcutSupport(bool b)
205{
206#if HAVE_KTEXTADDONS_KIO_SUPPORT
207 if (b) {
208 d->supportFeatures |= AllowWebShortcut;
209 } else {
210 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut);
211 }
212#else
213 Q_UNUSED(b);
214#endif
215}
216
217bool RichTextBrowser::webShortcutSupport() const
218{
219#if HAVE_KTEXTADDONS_KIO_SUPPORT
220 return d->supportFeatures & AllowWebShortcut;
221#else
222 return false;
223#endif
224}
225
226void RichTextBrowser::setSearchSupport(bool b)
227{
228 if (b) {
229 d->supportFeatures |= Search;
230 } else {
231 d->supportFeatures = (d->supportFeatures & ~Search);
232 }
233}
234
235bool RichTextBrowser::searchSupport() const
236{
237 return d->supportFeatures & Search;
238}
239
240void RichTextBrowser::setTextToSpeechSupport(bool b)
241{
242 if (b) {
243 d->supportFeatures |= TextToSpeech;
244 } else {
245 d->supportFeatures = (d->supportFeatures & ~TextToSpeech);
246 }
247}
248
249bool RichTextBrowser::textToSpeechSupport() const
250{
251 return d->supportFeatures & TextToSpeech;
252}
253
254void RichTextBrowser::addExtraMenuEntry(QMenu *menu, QPoint pos)
255{
256 Q_UNUSED(menu)
257 Q_UNUSED(pos)
258}
259
260void RichTextBrowser::slotUndoableClear()
261{
263 cursor.beginEditBlock();
264 cursor.movePosition(QTextCursor::Start);
266 cursor.removeSelectedText();
267 cursor.endEditBlock();
268}
269
270void RichTextBrowser::updateReadOnlyColor()
271{
272 if (isReadOnly()) {
273 QPalette p = palette();
274 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor);
275 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor);
276 setPalette(p);
277 }
278}
279
280static void richTextDeleteWord(QTextCursor cursor, QTextCursor::MoveOperation op)
281{
282 cursor.clearSelection();
284 cursor.removeSelectedText();
285}
286
287void RichTextBrowser::deleteWordBack()
288{
289 richTextDeleteWord(textCursor(), QTextCursor::PreviousWord);
290}
291
292void RichTextBrowser::deleteWordForward()
293{
294 richTextDeleteWord(textCursor(), QTextCursor::WordRight);
295}
296
297bool RichTextBrowser::event(QEvent *ev)
298{
299 if (ev->type() == QEvent::ShortcutOverride) {
300 auto e = static_cast<QKeyEvent *>(ev);
301 if (overrideShortcut(e)) {
302 e->accept();
303 return true;
304 }
305 } else if (ev->type() == QEvent::ApplicationPaletteChange) {
306 regenerateColorScheme();
307 }
308 return QTextEdit::event(ev);
309}
310
311void RichTextBrowser::wheelEvent(QWheelEvent *event)
312{
314 const int angleDeltaY{event->angleDelta().y()};
315 if (angleDeltaY > 0) {
316 zoomIn();
317 } else if (angleDeltaY < 0) {
318 zoomOut();
319 }
320 event->accept();
321 return;
322 }
324}
325
326bool RichTextBrowser::handleShortcut(QKeyEvent *event)
327{
328 const int key = event->key() | event->modifiers();
329
330 if (KStandardShortcut::copy().contains(key)) {
331 copy();
332 return true;
333 } else if (KStandardShortcut::paste().contains(key)) {
334 paste();
335 return true;
336 } else if (KStandardShortcut::cut().contains(key)) {
337 cut();
338 return true;
339 } else if (KStandardShortcut::undo().contains(key)) {
340 if (!isReadOnly()) {
341 undo();
342 }
343 return true;
344 } else if (KStandardShortcut::redo().contains(key)) {
345 if (!isReadOnly()) {
346 redo();
347 }
348 return true;
349 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
350 if (!isReadOnly()) {
351 deleteWordBack();
352 }
353 return true;
354 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
355 if (!isReadOnly()) {
356 deleteWordForward();
357 }
358 return true;
359 } else if (KStandardShortcut::backwardWord().contains(key)) {
361 cursor.movePosition(QTextCursor::PreviousWord);
363 return true;
364 } else if (KStandardShortcut::forwardWord().contains(key)) {
366 cursor.movePosition(QTextCursor::NextWord);
368 return true;
369 } else if (KStandardShortcut::next().contains(key)) {
371 bool moved = false;
372 qreal lastY = cursorRect(cursor).bottom();
373 qreal distance = 0;
374 do {
375 qreal y = cursorRect(cursor).bottom();
376 distance += qAbs(y - lastY);
377 lastY = y;
378 moved = cursor.movePosition(QTextCursor::Down);
379 } while (moved && distance < viewport()->height());
380
381 if (moved) {
382 cursor.movePosition(QTextCursor::Up);
384 }
386 return true;
387 } else if (KStandardShortcut::prior().contains(key)) {
389 bool moved = false;
390 qreal lastY = cursorRect(cursor).bottom();
391 qreal distance = 0;
392 do {
393 qreal y = cursorRect(cursor).bottom();
394 distance += qAbs(y - lastY);
395 lastY = y;
396 moved = cursor.movePosition(QTextCursor::Up);
397 } while (moved && distance < viewport()->height());
398
399 if (moved) {
400 cursor.movePosition(QTextCursor::Down);
402 }
404 return true;
405 } else if (KStandardShortcut::begin().contains(key)) {
407 cursor.movePosition(QTextCursor::Start);
409 return true;
410 } else if (KStandardShortcut::end().contains(key)) {
412 cursor.movePosition(QTextCursor::End);
414 return true;
415 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
417 cursor.movePosition(QTextCursor::StartOfLine);
419 return true;
420 } else if (KStandardShortcut::endOfLine().contains(key)) {
422 cursor.movePosition(QTextCursor::EndOfLine);
424 return true;
425 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
426 Q_EMIT findText();
427 return true;
428 } else if (KStandardShortcut::pasteSelection().contains(key)) {
430 if (!text.isEmpty()) {
431 insertPlainText(text); // TODO: check if this is html? (MiB)
432 }
433 return true;
434 } else if (event == QKeySequence::DeleteEndOfLine) {
436 QTextBlock block = cursor.block();
437 if (cursor.position() == block.position() + block.length() - 2) {
439 } else {
441 }
442 cursor.removeSelectedText();
444 return true;
445 }
446
447 return false;
448}
449
450bool RichTextBrowser::overrideShortcut(QKeyEvent *event)
451{
452 const int key = event->key() | event->modifiers();
453
454 if (KStandardShortcut::copy().contains(key)) {
455 return true;
456 } else if (KStandardShortcut::paste().contains(key)) {
457 return true;
458 } else if (KStandardShortcut::cut().contains(key)) {
459 return true;
460 } else if (KStandardShortcut::undo().contains(key)) {
461 return true;
462 } else if (KStandardShortcut::redo().contains(key)) {
463 return true;
464 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
465 return true;
466 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
467 return true;
468 } else if (KStandardShortcut::backwardWord().contains(key)) {
469 return true;
470 } else if (KStandardShortcut::forwardWord().contains(key)) {
471 return true;
472 } else if (KStandardShortcut::next().contains(key)) {
473 return true;
474 } else if (KStandardShortcut::prior().contains(key)) {
475 return true;
476 } else if (KStandardShortcut::begin().contains(key)) {
477 return true;
478 } else if (KStandardShortcut::end().contains(key)) {
479 return true;
480 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
481 return true;
482 } else if (KStandardShortcut::endOfLine().contains(key)) {
483 return true;
484 } else if (KStandardShortcut::pasteSelection().contains(key)) {
485 return true;
486 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
487 return true;
488 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) {
489 return true;
490 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit
491 return true;
492 } else if (event == QKeySequence::DeleteEndOfLine) {
493 return true;
494 }
495 return false;
496}
497
498void RichTextBrowser::keyPressEvent(QKeyEvent *event)
499{
500 const bool isControlClicked = event->modifiers() & Qt::ControlModifier;
501 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier;
502 if (handleShortcut(event)) {
503 event->accept();
504 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) {
505 moveLineUpDown(true);
506 event->accept();
507 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) {
508 moveLineUpDown(false);
509 event->accept();
510 } else if (event->key() == Qt::Key_Up && isControlClicked) {
511 moveCursorBeginUpDown(true);
512 event->accept();
513 } else if (event->key() == Qt::Key_Down && isControlClicked) {
514 moveCursorBeginUpDown(false);
515 event->accept();
516 } else {
518 }
519}
520
521int RichTextBrowser::zoomFactor() const
522{
523 int pourcentage = 100;
524 const QFont f = font();
525 if (d->mInitialFontSize != f.pointSize()) {
526 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize;
527 }
528 return pourcentage;
529}
530
531void RichTextBrowser::slotZoomReset()
532{
533 QFont f = font();
534 if (d->mInitialFontSize != f.pointSize()) {
535 f.setPointSize(d->mInitialFontSize);
536 setFont(f);
537 }
538}
539
540void RichTextBrowser::moveCursorBeginUpDown(bool moveUp)
541{
545 cursor.clearSelection();
546 move.movePosition(QTextCursor::StartOfBlock);
548 move.endEditBlock();
550}
551
552void RichTextBrowser::moveLineUpDown(bool moveUp)
553{
557
558 const bool hasSelection = cursor.hasSelection();
559
560 if (hasSelection) {
561 move.setPosition(cursor.selectionStart());
562 move.movePosition(QTextCursor::StartOfBlock);
563 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
565 } else {
566 move.movePosition(QTextCursor::StartOfBlock);
568 }
569 const QString text = move.selectedText();
570
572 move.removeSelectedText();
573
574 if (moveUp) {
575 move.movePosition(QTextCursor::PreviousBlock);
576 move.insertBlock();
577 move.movePosition(QTextCursor::Left);
578 } else {
579 move.movePosition(QTextCursor::EndOfBlock);
580 if (move.atBlockStart()) { // empty block
581 move.movePosition(QTextCursor::NextBlock);
582 move.insertBlock();
583 move.movePosition(QTextCursor::Left);
584 } else {
585 move.insertBlock();
586 }
587 }
588
589 int start = move.position();
590 move.clearSelection();
591 move.insertText(text);
592 int end = move.position();
593
594 if (hasSelection) {
595 move.setPosition(end);
596 move.setPosition(start, QTextCursor::KeepAnchor);
597 } else {
598 move.setPosition(start);
599 }
600 move.endEditBlock();
601
603}
604
605#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
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
bool atBlockEnd() const const
bool atBlockStart() 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 Mon Nov 4 2024 16:29:59 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.