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