1 /*
2 SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
3 SPDX-FileCopyrightText: 2010-2018 Mladen Milinkovic <max@smoothware.net>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "simplerichtextedit.h"
9
10 #include "application.h"
11 #include "actions/useractionnames.h"
12 #include "core/undo/undostack.h"
13 #include "core/richdocument.h"
14 #include "dialogs/subtitlecolordialog.h"
15
16 #include <QRegExp>
17 #include <QEvent>
18 #include <QMenu>
19 #include <QShortcutEvent>
20 #include <QContextMenuEvent>
21 #include <QFocusEvent>
22 #include <QKeyEvent>
23 #include <QAction>
24 #include <QIcon>
25 #include <QDebug>
26
27 #include <KStandardShortcut>
28 #include <KLocalizedString>
29
30 using namespace SubtitleComposer;
31
SimpleRichTextEdit(QWidget * parent)32 SimpleRichTextEdit::SimpleRichTextEdit(QWidget *parent)
33 : KTextEdit(parent)
34 {
35 enableFindReplace(false);
36 setCheckSpellingEnabled(true);
37
38 setAutoFormatting(AutoNone);
39 setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
40
41 setTextInteractionFlags(Qt::TextEditorInteraction);
42
43 connect(app(), &Application::actionsReady, this, &SimpleRichTextEdit::setupActions);
44
45 QMenu *menu = createStandardContextMenu();
46 menu->setParent(this);
47 QList<QAction *> actions = menu->actions();
48 m_insertUnicodeControlCharMenu = 0;
49 for(QList<QAction *>::ConstIterator it = actions.constBegin(), end = actions.constEnd(); it != end; ++it) {
50 if((*it)->menu()) {
51 // this depends on Qt private implementation but at least is guaranteed
52 // to behave reasonably if that implementation changes in the future.
53 if(!strcmp((*it)->menu()->metaObject()->className(), "QUnicodeControlCharacterMenu")) {
54 m_insertUnicodeControlCharMenu = (*it)->menu();
55 break;
56 }
57 }
58 }
59 }
60
~SimpleRichTextEdit()61 SimpleRichTextEdit::~SimpleRichTextEdit()
62 {
63 if(m_insertUnicodeControlCharMenu)
64 delete m_insertUnicodeControlCharMenu->parent();
65 }
66
67 void
changeTextColor()68 SimpleRichTextEdit::changeTextColor()
69 {
70 QColor color = SubtitleComposer::SubtitleColorDialog::getColor(textColor(), this);
71 if(color.isValid()) {
72 if(color.rgba() == 0) {
73 QTextCursor cursor(textCursor());
74 QTextCharFormat format;
75 format.setForeground(QBrush(Qt::NoBrush));
76 cursor.mergeCharFormat(format);
77 setTextCursor(cursor);
78 } else {
79 setTextColor(color);
80 }
81 }
82 }
83
84 void
deleteText()85 SimpleRichTextEdit::deleteText()
86 {
87 QTextCursor cursor = textCursor();
88 if(cursor.hasSelection())
89 cursor.removeSelectedText();
90 else
91 cursor.deleteChar();
92 }
93
94 void
undoableClear()95 SimpleRichTextEdit::undoableClear()
96 {
97 QTextCursor cursor = textCursor();
98 cursor.beginEditBlock();
99 cursor.select(QTextCursor::Document);
100 cursor.removeSelectedText();
101 cursor.endEditBlock();
102 }
103
104 void
setSelection(int startIndex,int endIndex)105 SimpleRichTextEdit::setSelection(int startIndex, int endIndex)
106 {
107 QTextCursor cursor(document());
108 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, startIndex);
109 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, endIndex - startIndex + 1);
110 setTextCursor(cursor);
111 }
112
113 void
clearSelection()114 SimpleRichTextEdit::clearSelection()
115 {
116 QTextCursor cursor(textCursor());
117 cursor.clearSelection();
118 setTextCursor(cursor);
119 }
120
121 void
setupWordUnderPositionCursor(const QPoint & globalPos)122 SimpleRichTextEdit::setupWordUnderPositionCursor(const QPoint &globalPos)
123 {
124 // Get the word under the (mouse-)cursor with apostrophes at the start/end
125 m_selectedWordCursor = cursorForPosition(mapFromGlobal(globalPos));
126 m_selectedWordCursor.clearSelection();
127 m_selectedWordCursor.select(QTextCursor::WordUnderCursor);
128
129 QString selectedWord = m_selectedWordCursor.selectedText();
130
131 // Clear the selection again, we re-select it below (without the apostrophes).
132 m_selectedWordCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, selectedWord.size());
133 if(selectedWord.startsWith('\'') || selectedWord.startsWith('\"')) {
134 selectedWord = selectedWord.right(selectedWord.size() - 1);
135 m_selectedWordCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
136 }
137
138 if(selectedWord.endsWith('\'') || selectedWord.endsWith('\"'))
139 selectedWord.chop(1);
140
141 m_selectedWordCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size());
142 }
143
144 void
addToIgnoreList()145 SimpleRichTextEdit::addToIgnoreList()
146 {
147 highlighter()->ignoreWord(m_selectedWordCursor.selectedText());
148 highlighter()->rehighlight();
149 m_selectedWordCursor.clearSelection();
150 }
151
152 void
addToDictionary()153 SimpleRichTextEdit::addToDictionary()
154 {
155 highlighter()->addWordToDictionary(m_selectedWordCursor.selectedText());
156 highlighter()->rehighlight();
157 m_selectedWordCursor.clearSelection();
158 }
159
160 void
replaceWithSuggestion()161 SimpleRichTextEdit::replaceWithSuggestion()
162 {
163 QAction *action = qobject_cast<QAction *>(sender());
164 if(action) {
165 m_selectedWordCursor.insertText(action->text());
166 setTextCursor(m_selectedWordCursor);
167 m_selectedWordCursor.clearSelection();
168 }
169 }
170
171 QMenu *
createContextMenu(const QPoint & mouseGlobalPos)172 SimpleRichTextEdit::createContextMenu(const QPoint &mouseGlobalPos)
173 {
174 Qt::TextInteractionFlags interactionFlags = this->textInteractionFlags();
175 QTextDocument *document = this->document();
176 QTextCursor cursor = textCursor();
177
178 const bool showTextSelectionActions = (Qt::TextEditable | Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse) & interactionFlags;
179
180 QMenu *menu = new QMenu(this);
181
182 if(interactionFlags & Qt::TextEditable) {
183 m_actions[Undo]->setEnabled(app()->undoStack()->canUndo());
184 menu->addAction(m_actions[Undo]);
185
186 m_actions[Redo]->setEnabled(app()->undoStack()->canRedo());
187 menu->addAction(m_actions[Redo]);
188
189 menu->addSeparator();
190
191 m_actions[Cut]->setEnabled(cursor.hasSelection());
192 menu->addAction(m_actions[Cut]);
193 }
194
195 if(showTextSelectionActions) {
196 m_actions[Copy]->setEnabled(cursor.hasSelection());
197 menu->addAction(m_actions[Copy]);
198 }
199
200 if(interactionFlags & Qt::TextEditable) {
201 #if !defined(QT_NO_CLIPBOARD)
202 m_actions[Paste]->setEnabled(canPaste());
203 menu->addAction(m_actions[Paste]);
204 #endif
205 m_actions[Delete]->setEnabled(cursor.hasSelection());
206 menu->addAction(m_actions[Delete]);
207
208 m_actions[Clear]->setEnabled(!document->isEmpty());
209 menu->addAction(m_actions[Clear]);
210
211 if(m_insertUnicodeControlCharMenu && interactionFlags & Qt::TextEditable) {
212 menu->addSeparator();
213 menu->addMenu(m_insertUnicodeControlCharMenu);
214 }
215 }
216
217 if(showTextSelectionActions) {
218 menu->addSeparator();
219
220 m_actions[SelectAll]->setEnabled(!document->isEmpty());
221 menu->addAction(m_actions[SelectAll]);
222 }
223
224 if(interactionFlags & Qt::TextEditable) {
225 menu->addSeparator();
226
227 m_actions[ToggleBold]->setChecked(fontBold());
228 menu->addAction(m_actions[ToggleBold]);
229
230 m_actions[ToggleItalic]->setChecked(fontItalic());
231 menu->addAction(m_actions[ToggleItalic]);
232
233 m_actions[ToggleUnderline]->setChecked(fontUnderline());
234 menu->addAction(m_actions[ToggleUnderline]);
235
236 m_actions[ToggleStrikeOut]->setChecked(fontStrikeOut());
237 menu->addAction(m_actions[ToggleStrikeOut]);
238
239 menu->addAction(m_actions[ChangeTextColor]);
240
241 menu->addSeparator();
242
243 m_actions[CheckSpelling]->setEnabled(!document->isEmpty());
244 menu->addAction(m_actions[CheckSpelling]);
245
246 m_actions[ToggleAutoSpellChecking]->setChecked(checkSpellingEnabled());
247 menu->addAction(m_actions[ToggleAutoSpellChecking]);
248
249 if(checkSpellingEnabled()) {
250 setupWordUnderPositionCursor(mouseGlobalPos);
251
252 QString selectedWord = m_selectedWordCursor.selectedText();
253 if(!selectedWord.isEmpty() && highlighter() && highlighter()->isWordMisspelled(selectedWord)) {
254 QMenu *suggestionsMenu = menu->addMenu(i18n("Suggestions"));
255 suggestionsMenu->addAction(i18n("Ignore"), this, &SimpleRichTextEdit::addToIgnoreList);
256 suggestionsMenu->addAction(i18n("Add to Dictionary"), this, &SimpleRichTextEdit::addToDictionary);
257 suggestionsMenu->addSeparator();
258 QStringList suggestions = highlighter()->suggestionsForWord(m_selectedWordCursor.selectedText());
259 if(suggestions.empty())
260 suggestionsMenu->addAction(i18n("No suggestions"))->setEnabled(false);
261 else {
262 for(QStringList::ConstIterator it = suggestions.constBegin(), end = suggestions.constEnd(); it != end; ++it)
263 suggestionsMenu->addAction(*it, this, &SimpleRichTextEdit::replaceWithSuggestion);
264 }
265 }
266 }
267
268 menu->addSeparator();
269
270 m_actions[AllowTabulations]->setChecked(!tabChangesFocus());
271 menu->addAction(m_actions[AllowTabulations]);
272 }
273
274 return menu;
275 }
276
277 void
contextMenuEvent(QContextMenuEvent * event)278 SimpleRichTextEdit::contextMenuEvent(QContextMenuEvent *event)
279 {
280 QMenu *menu = createContextMenu(event->globalPos());
281 menu->exec(event->globalPos());
282 delete menu;
283 }
284
285 bool
event(QEvent * event)286 SimpleRichTextEdit::event(QEvent *event)
287 {
288 if(event->type() == QEvent::ShortcutOverride) {
289 const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
290 const QKeySequence key(keyEvent->modifiers() + keyEvent->key());
291
292 for(int i = 0; i < ActionCount; i++) {
293 if(m_actions.at(i)->shortcuts().contains(key)) {
294 event->accept();
295 return true;
296 }
297 }
298 }
299
300 return KTextEdit::event(event);
301 }
302
303 void
keyPressEvent(QKeyEvent * event)304 SimpleRichTextEdit::keyPressEvent(QKeyEvent *event)
305 {
306 const QKeySequence key(event->modifiers() + event->key());
307
308 for(int i = 0; i < ActionCount; i++) {
309 if(m_actions.at(i)->shortcuts().contains(key)) {
310 m_actions.at(i)->trigger();
311 if(i == Undo || i == Redo) {
312 RichDocument *doc = qobject_cast<RichDocument *>(document());
313 if(doc)
314 setTextCursor(*doc->undoableCursor());
315 }
316 return;
317 }
318 }
319
320 KTextEdit::keyPressEvent(event);
321 }
322
323 static void
setupActionCommon(QAction * act,const char * appActionId)324 setupActionCommon(QAction *act, const char *appActionId)
325 {
326 QAction *appAction = qobject_cast<QAction *>(app()->action(appActionId));
327 QObject::connect(appAction, &QAction::changed, act, [act, appAction](){ act->setShortcut(appAction->shortcut()); });
328 act->setShortcuts(appAction->shortcuts());
329 }
330
331 void
setupActions()332 SimpleRichTextEdit::setupActions()
333 {
334 m_actions[Undo] = app()->action(ACT_UNDO);
335 m_actions[Redo] = app()->action(ACT_REDO);
336
337 QAction *act = m_actions[Cut] = new QAction(this);
338 act->setIcon(QIcon::fromTheme("edit-cut"));
339 act->setText(i18n("Cut"));
340 act->setShortcuts(KStandardShortcut::cut());
341 connect(act, &QAction::triggered, this, &QTextEdit::cut);
342
343 act = m_actions[Copy] = new QAction(this);
344 act->setIcon(QIcon::fromTheme("edit-copy"));
345 act->setText(i18n("Copy"));
346 act->setShortcuts(KStandardShortcut::copy());
347 connect(act, &QAction::triggered, this, &QTextEdit::copy);
348
349 #ifndef QT_NO_CLIPBOARD
350 act = m_actions[Paste] = new QAction(this);
351 act->setIcon(QIcon::fromTheme("edit-paste"));
352 act->setText(i18n("Paste"));
353 act->setShortcuts(KStandardShortcut::paste());
354 connect(act, &QAction::triggered, this, &QTextEdit::paste);
355 #endif
356
357 act = m_actions[Delete] = new QAction(this);
358 act->setIcon(QIcon::fromTheme("edit-delete"));
359 act->setText(i18n("Delete"));
360 act->setShortcut(QKeySequence::Delete);
361 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::deleteText);
362
363 act = m_actions[Clear] = new QAction(this);
364 act->setIcon(QIcon::fromTheme("edit-clear"));
365 act->setText(i18nc("@action:inmenu Clear all text", "Clear"));
366 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::undoableClear);
367
368 act = m_actions[SelectAll] = new QAction(this);
369 act->setIcon(QIcon::fromTheme("edit-select-all"));
370 act->setText(i18n("Select All"));
371 setupActionCommon(act, ACT_SELECT_ALL_LINES);
372 connect(act, &QAction::triggered, this, &QTextEdit::selectAll);
373
374 act = m_actions[ToggleBold] = new QAction(this);
375 act->setIcon(QIcon::fromTheme("format-text-bold"));
376 act->setText(i18nc("@action:inmenu Toggle bold style", "Bold"));
377 act->setCheckable(true);
378 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_BOLD);
379 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontBold);
380
381 act = m_actions[ToggleItalic] = new QAction(this);
382 act->setIcon(QIcon::fromTheme("format-text-italic"));
383 act->setText(i18nc("@action:inmenu Toggle italic style", "Italic"));
384 act->setCheckable(true);
385 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_ITALIC);
386 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontItalic);
387
388 act = m_actions[ToggleUnderline] = new QAction(this);
389 act->setIcon(QIcon::fromTheme("format-text-underline"));
390 act->setText(i18nc("@action:inmenu Toggle underline style", "Underline"));
391 act->setCheckable(true);
392 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_UNDERLINE);
393 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontUnderline);
394
395 act = m_actions[ToggleStrikeOut] = new QAction(this);
396 act->setIcon(QIcon::fromTheme("format-text-strikethrough"));
397 act->setText(i18nc("@action:inmenu Toggle strike through style", "Strike Through"));
398 act->setCheckable(true);
399 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH);
400 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontStrikeOut);
401
402 act = m_actions[ChangeTextColor] = new QAction(this);
403 act->setIcon(QIcon::fromTheme("format-text-color"));
404 act->setText(i18nc("@action:inmenu Change Text Color", "Text Color"));
405 setupActionCommon(act, ACT_CHANGE_SELECTED_LINES_TEXT_COLOR);
406 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::changeTextColor);
407
408 act = m_actions[CheckSpelling] = new QAction(this);
409 act->setIcon(QIcon::fromTheme("tools-check-spelling"));
410 act->setText(i18n("Check Spelling..."));
411 connect(act, &QAction::triggered, app(), &Application::spellCheck);
412 connect(act, &QAction::triggered, this, &KTextEdit::checkSpelling);
413
414 act = m_actions[ToggleAutoSpellChecking] = new QAction(this);
415 act->setText(i18n("Auto Spell Check"));
416 act->setCheckable(true);
417 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleAutoSpellChecking);
418
419 act = m_actions[AllowTabulations] = new QAction(this);
420 act->setText(i18n("Allow Tabulations"));
421 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleTabChangesFocus);
422 }
423
424