File indexing completed on 2024-06-09 04:53:16
0001 /* 0002 SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar> 0003 SPDX-FileCopyrightText: 2010-2022 Mladen Milinkovic <max@smoothware.net> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "simplerichtextedit.h" 0009 0010 #include "appglobal.h" 0011 #include "application.h" 0012 #include "actions/useractionnames.h" 0013 #include "core/undo/undostack.h" 0014 #include "core/richtext/richdocument.h" 0015 #include "helpers/common.h" 0016 #include "dialogs/subtitleclassdialog.h" 0017 #include "dialogs/subtitlecolordialog.h" 0018 #include "dialogs/subtitlevoicedialog.h" 0019 0020 #include <QEvent> 0021 #include <QMenu> 0022 #include <QShortcutEvent> 0023 #include <QContextMenuEvent> 0024 #include <QFocusEvent> 0025 #include <QKeyEvent> 0026 #include <QAction> 0027 #include <QIcon> 0028 #include <QDebug> 0029 0030 #include <KStandardShortcut> 0031 #include <KLocalizedString> 0032 0033 using namespace SubtitleComposer; 0034 0035 SimpleRichTextEdit::SimpleRichTextEdit(QWidget *parent) 0036 : KTextEdit(parent) 0037 { 0038 enableFindReplace(false); 0039 setCheckSpellingEnabled(true); 0040 0041 setAutoFormatting(AutoNone); 0042 setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); 0043 0044 setTextInteractionFlags(Qt::TextEditorInteraction); 0045 0046 connect(app(), &Application::actionsReady, this, &SimpleRichTextEdit::setupActions); 0047 0048 QMenu *menu = QTextEdit::createStandardContextMenu(); 0049 menu->setParent(this); 0050 QList<QAction *> actions = menu->actions(); 0051 m_insertUnicodeControlCharMenu = 0; 0052 for(QList<QAction *>::ConstIterator it = actions.constBegin(), end = actions.constEnd(); it != end; ++it) { 0053 if((*it)->menu()) { 0054 // this depends on Qt private implementation but at least is guaranteed 0055 // to behave reasonably if that implementation changes in the future. 0056 if(!strcmp((*it)->menu()->metaObject()->className(), "QUnicodeControlCharacterMenu")) { 0057 m_insertUnicodeControlCharMenu = (*it)->menu(); 0058 break; 0059 } 0060 } 0061 } 0062 } 0063 0064 SimpleRichTextEdit::~SimpleRichTextEdit() 0065 { 0066 if(m_insertUnicodeControlCharMenu) 0067 delete m_insertUnicodeControlCharMenu->parent(); 0068 } 0069 0070 const QTextCharFormat 0071 SimpleRichTextEdit::charFormat() const 0072 { 0073 QTextCursor c = textCursor(); 0074 if(c.hasSelection()) 0075 c.movePosition(QTextCursor::NextCharacter); 0076 return c.charFormat(); 0077 } 0078 0079 void 0080 SimpleRichTextEdit::changeTextColor() 0081 { 0082 QColor color = SubtitleColorDialog::getColor(textColor(), this); 0083 if(color.isValid()) { 0084 if(color.rgba() == 0) { 0085 QTextCursor cursor(textCursor()); 0086 QTextCharFormat format; 0087 format.setForeground(QBrush(Qt::NoBrush)); 0088 cursor.mergeCharFormat(format); 0089 setTextCursor(cursor); 0090 } else { 0091 setTextColor(color); 0092 } 0093 } 0094 } 0095 0096 static const QStringList 0097 allClasses(RichDocument *doc) 0098 { 0099 QSet<QString> vs = doc->stylesheet()->classes(); 0100 const Subtitle *s = appSubtitle(); 0101 for(int i = 0, n = s->count(); i < n; i++) { 0102 const RichDocument *d = s->line(i)->primaryDoc(); 0103 const QVector<QTextFormat> vf = d->allFormats(); 0104 for(const QTextFormat &f: vf) { 0105 if(!f.hasProperty(RichDocument::Class)) 0106 continue; 0107 vs.unite(f.property(RichDocument::Class).value<QSet<QString>>()); 0108 } 0109 } 0110 return vs.values(); 0111 } 0112 0113 void 0114 SimpleRichTextEdit::changeTextClass() 0115 { 0116 RichDocument *doc = static_cast<RichDocument *>(document()); 0117 QTextCursor cursor(textCursor()); 0118 QSet<QString> cl = cursor.charFormat().property(RichDocument::Class).value<QSet<QString>>(); 0119 const QString cc = cl.values().join(QChar::Space); 0120 const QString klass = SubtitleClassDialog::getClass(allClasses(doc), cc, this); 0121 if(!klass.isNull()) { 0122 staticRE$(reWords, "\\S+", REu); 0123 QRegularExpressionMatchIterator mi = reWords.globalMatch(klass); 0124 cl.clear(); 0125 while(mi.hasNext()) { 0126 const QRegularExpressionMatch m = mi.next(); 0127 cl.insert(m.captured(0)); 0128 } 0129 QTextCharFormat format; 0130 format.setProperty(RichDocument::Class, QVariant::fromValue(cl)); 0131 cursor.mergeCharFormat(format); 0132 setTextCursor(cursor); 0133 } 0134 } 0135 0136 static const QStringList 0137 allVoices() 0138 { 0139 QSet<QString> vs; 0140 const Subtitle *s = appSubtitle(); 0141 for(int i = 0, n = s->count(); i < n; i++) { 0142 const RichDocument *d = s->line(i)->primaryDoc(); 0143 const QVector<QTextFormat> vf = d->allFormats(); 0144 for(const QTextFormat &f: vf) { 0145 if(!f.hasProperty(RichDocument::Voice)) 0146 continue; 0147 vs.insert(f.property(RichDocument::Voice).toString()); 0148 } 0149 } 0150 return vs.values(); 0151 } 0152 0153 void 0154 SimpleRichTextEdit::changeTextVoice() 0155 { 0156 QTextCursor cursor(textCursor()); 0157 const QString cv = cursor.charFormat().property(RichDocument::Voice).value<QString>(); 0158 const QString voice = SubtitleVoiceDialog::getVoice(allVoices(), cv, this); 0159 if(!voice.isNull()) { 0160 QTextCharFormat format; 0161 format.setProperty(RichDocument::Voice, QVariant::fromValue(voice)); 0162 cursor.mergeCharFormat(format); 0163 setTextCursor(cursor); 0164 } 0165 } 0166 0167 void 0168 SimpleRichTextEdit::deleteText() 0169 { 0170 QTextCursor cursor = textCursor(); 0171 if(cursor.hasSelection()) 0172 cursor.removeSelectedText(); 0173 else 0174 cursor.deleteChar(); 0175 } 0176 0177 void 0178 SimpleRichTextEdit::undoableClear() 0179 { 0180 QTextCursor cursor = textCursor(); 0181 cursor.beginEditBlock(); 0182 cursor.select(QTextCursor::Document); 0183 cursor.removeSelectedText(); 0184 cursor.endEditBlock(); 0185 } 0186 0187 void 0188 SimpleRichTextEdit::setSelection(int startIndex, int endIndex) 0189 { 0190 QTextCursor cursor(document()); 0191 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, startIndex); 0192 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, endIndex - startIndex + 1); 0193 setTextCursor(cursor); 0194 } 0195 0196 void 0197 SimpleRichTextEdit::clearSelection() 0198 { 0199 QTextCursor cursor(textCursor()); 0200 cursor.clearSelection(); 0201 setTextCursor(cursor); 0202 } 0203 0204 void 0205 SimpleRichTextEdit::setupWordUnderPositionCursor(const QPoint &globalPos) 0206 { 0207 // Get the word under the (mouse-)cursor with apostrophes at the start/end 0208 m_selectedWordCursor = cursorForPosition(mapFromGlobal(globalPos)); 0209 m_selectedWordCursor.clearSelection(); 0210 m_selectedWordCursor.select(QTextCursor::WordUnderCursor); 0211 0212 QString selectedWord = m_selectedWordCursor.selectedText(); 0213 0214 // Clear the selection again, we re-select it below (without the apostrophes). 0215 m_selectedWordCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, selectedWord.size()); 0216 if(selectedWord.startsWith('\'') || selectedWord.startsWith('\"')) { 0217 selectedWord = selectedWord.right(selectedWord.size() - 1); 0218 m_selectedWordCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor); 0219 } 0220 0221 if(selectedWord.endsWith('\'') || selectedWord.endsWith('\"')) 0222 selectedWord.chop(1); 0223 0224 m_selectedWordCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size()); 0225 } 0226 0227 void 0228 SimpleRichTextEdit::addToIgnoreList() 0229 { 0230 highlighter()->ignoreWord(m_selectedWordCursor.selectedText()); 0231 highlighter()->rehighlight(); 0232 m_selectedWordCursor.clearSelection(); 0233 } 0234 0235 void 0236 SimpleRichTextEdit::addToDictionary() 0237 { 0238 highlighter()->addWordToDictionary(m_selectedWordCursor.selectedText()); 0239 highlighter()->rehighlight(); 0240 m_selectedWordCursor.clearSelection(); 0241 } 0242 0243 void 0244 SimpleRichTextEdit::replaceWithSuggestion() 0245 { 0246 QAction *action = qobject_cast<QAction *>(sender()); 0247 if(action) { 0248 m_selectedWordCursor.insertText(action->text()); 0249 setTextCursor(m_selectedWordCursor); 0250 m_selectedWordCursor.clearSelection(); 0251 } 0252 } 0253 0254 QMenu * 0255 SimpleRichTextEdit::createContextMenu(const QPoint &mouseGlobalPos) 0256 { 0257 Qt::TextInteractionFlags interactionFlags = this->textInteractionFlags(); 0258 QTextDocument *document = this->document(); 0259 QTextCursor cursor = textCursor(); 0260 0261 const bool showTextSelectionActions = (Qt::TextEditable | Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse) & interactionFlags; 0262 0263 QMenu *menu = new QMenu(this); 0264 0265 if(interactionFlags & Qt::TextEditable) { 0266 m_actions[Undo]->setEnabled(appUndoStack()->canUndo()); 0267 menu->addAction(m_actions[Undo]); 0268 0269 m_actions[Redo]->setEnabled(appUndoStack()->canRedo()); 0270 menu->addAction(m_actions[Redo]); 0271 0272 menu->addSeparator(); 0273 0274 m_actions[Cut]->setEnabled(cursor.hasSelection()); 0275 menu->addAction(m_actions[Cut]); 0276 } 0277 0278 if(showTextSelectionActions) { 0279 m_actions[Copy]->setEnabled(cursor.hasSelection()); 0280 menu->addAction(m_actions[Copy]); 0281 } 0282 0283 if(interactionFlags & Qt::TextEditable) { 0284 #if !defined(QT_NO_CLIPBOARD) 0285 m_actions[Paste]->setEnabled(canPaste()); 0286 menu->addAction(m_actions[Paste]); 0287 #endif 0288 m_actions[Delete]->setEnabled(cursor.hasSelection()); 0289 menu->addAction(m_actions[Delete]); 0290 0291 m_actions[Clear]->setEnabled(!document->isEmpty()); 0292 menu->addAction(m_actions[Clear]); 0293 0294 if(m_insertUnicodeControlCharMenu && interactionFlags & Qt::TextEditable) { 0295 menu->addSeparator(); 0296 menu->addMenu(m_insertUnicodeControlCharMenu); 0297 } 0298 } 0299 0300 if(showTextSelectionActions) { 0301 menu->addSeparator(); 0302 0303 m_actions[SelectAll]->setEnabled(!document->isEmpty()); 0304 menu->addAction(m_actions[SelectAll]); 0305 } 0306 0307 if(interactionFlags & Qt::TextEditable) { 0308 menu->addSeparator(); 0309 0310 const QTextCharFormat fmt = charFormat(); 0311 0312 m_actions[ToggleBold]->setChecked(fmt.fontWeight() == QFont::Bold); 0313 menu->addAction(m_actions[ToggleBold]); 0314 0315 m_actions[ToggleItalic]->setChecked(fmt.fontItalic()); 0316 menu->addAction(m_actions[ToggleItalic]); 0317 0318 m_actions[ToggleUnderline]->setChecked(fmt.fontUnderline()); 0319 menu->addAction(m_actions[ToggleUnderline]); 0320 0321 m_actions[ToggleStrikeOut]->setChecked(fmt.fontStrikeOut()); 0322 menu->addAction(m_actions[ToggleStrikeOut]); 0323 0324 menu->addAction(m_actions[ChangeTextColor]); 0325 0326 menu->addSeparator(); 0327 0328 m_actions[CheckSpelling]->setEnabled(!document->isEmpty()); 0329 menu->addAction(m_actions[CheckSpelling]); 0330 0331 m_actions[ToggleAutoSpellChecking]->setChecked(checkSpellingEnabled()); 0332 menu->addAction(m_actions[ToggleAutoSpellChecking]); 0333 0334 if(checkSpellingEnabled()) { 0335 setupWordUnderPositionCursor(mouseGlobalPos); 0336 0337 QString selectedWord = m_selectedWordCursor.selectedText(); 0338 if(!selectedWord.isEmpty() && highlighter() && highlighter()->isWordMisspelled(selectedWord)) { 0339 QMenu *suggestionsMenu = menu->addMenu(i18n("Suggestions")); 0340 suggestionsMenu->addAction(i18n("Ignore"), this, &SimpleRichTextEdit::addToIgnoreList); 0341 suggestionsMenu->addAction(i18n("Add to Dictionary"), this, &SimpleRichTextEdit::addToDictionary); 0342 suggestionsMenu->addSeparator(); 0343 QStringList suggestions = highlighter()->suggestionsForWord(m_selectedWordCursor.selectedText()); 0344 if(suggestions.empty()) 0345 suggestionsMenu->addAction(i18n("No suggestions"))->setEnabled(false); 0346 else { 0347 for(QStringList::ConstIterator it = suggestions.constBegin(), end = suggestions.constEnd(); it != end; ++it) 0348 suggestionsMenu->addAction(*it, this, &SimpleRichTextEdit::replaceWithSuggestion); 0349 } 0350 } 0351 } 0352 0353 menu->addSeparator(); 0354 0355 m_actions[AllowTabulations]->setChecked(!tabChangesFocus()); 0356 menu->addAction(m_actions[AllowTabulations]); 0357 } 0358 0359 return menu; 0360 } 0361 0362 void 0363 SimpleRichTextEdit::contextMenuEvent(QContextMenuEvent *event) 0364 { 0365 QMenu *menu = createContextMenu(event->globalPos()); 0366 menu->exec(event->globalPos()); 0367 delete menu; 0368 } 0369 0370 bool 0371 SimpleRichTextEdit::event(QEvent *event) 0372 { 0373 if(event->type() == QEvent::ShortcutOverride) { 0374 const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); 0375 const QKeySequence key(keyEvent->modifiers() | keyEvent->key()); 0376 0377 for(int i = 0; i < ActionCount; i++) { 0378 if(m_actions.at(i)->shortcuts().contains(key)) { 0379 event->accept(); 0380 return true; 0381 } 0382 } 0383 } 0384 0385 return KTextEdit::event(event); 0386 } 0387 0388 void 0389 SimpleRichTextEdit::keyPressEvent(QKeyEvent *event) 0390 { 0391 const QKeySequence key(event->modifiers() | event->key()); 0392 0393 for(int i = 0; i < ActionCount; i++) { 0394 if(m_actions.at(i)->shortcuts().contains(key)) { 0395 m_actions.at(i)->trigger(); 0396 if(i == Undo || i == Redo) { 0397 RichDocument *doc = qobject_cast<RichDocument *>(document()); 0398 if(doc) 0399 setTextCursor(*doc->undoableCursor()); 0400 } 0401 return; 0402 } 0403 } 0404 0405 KTextEdit::keyPressEvent(event); 0406 } 0407 0408 static void 0409 setupActionCommon(QAction *act, const char *appActionId) 0410 { 0411 QAction *appAction = qobject_cast<QAction *>(app()->action(appActionId)); 0412 QObject::connect(appAction, &QAction::changed, act, [act, appAction](){ act->setShortcut(appAction->shortcut()); }); 0413 act->setShortcuts(appAction->shortcuts()); 0414 } 0415 0416 void 0417 SimpleRichTextEdit::setupActions() 0418 { 0419 m_actions[Undo] = app()->action(ACT_UNDO); 0420 m_actions[Redo] = app()->action(ACT_REDO); 0421 0422 QAction *act = m_actions[Cut] = new QAction(this); 0423 act->setIcon(QIcon::fromTheme("edit-cut")); 0424 act->setText(i18n("Cut")); 0425 act->setShortcuts(KStandardShortcut::cut()); 0426 connect(act, &QAction::triggered, this, &QTextEdit::cut); 0427 0428 act = m_actions[Copy] = new QAction(this); 0429 act->setIcon(QIcon::fromTheme("edit-copy")); 0430 act->setText(i18n("Copy")); 0431 act->setShortcuts(KStandardShortcut::copy()); 0432 connect(act, &QAction::triggered, this, &QTextEdit::copy); 0433 0434 #ifndef QT_NO_CLIPBOARD 0435 act = m_actions[Paste] = new QAction(this); 0436 act->setIcon(QIcon::fromTheme("edit-paste")); 0437 act->setText(i18n("Paste")); 0438 act->setShortcuts(KStandardShortcut::paste()); 0439 connect(act, &QAction::triggered, this, &QTextEdit::paste); 0440 #endif 0441 0442 act = m_actions[Delete] = new QAction(this); 0443 act->setIcon(QIcon::fromTheme("edit-delete")); 0444 act->setText(i18n("Delete")); 0445 act->setShortcut(QKeySequence::Delete); 0446 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::deleteText); 0447 0448 act = m_actions[Clear] = new QAction(this); 0449 act->setIcon(QIcon::fromTheme("edit-clear")); 0450 act->setText(i18nc("@action:inmenu Clear all text", "Clear")); 0451 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::undoableClear); 0452 0453 act = m_actions[SelectAll] = new QAction(this); 0454 act->setIcon(QIcon::fromTheme("edit-select-all")); 0455 act->setText(i18n("Select All")); 0456 setupActionCommon(act, ACT_SELECT_ALL_LINES); 0457 connect(act, &QAction::triggered, this, &QTextEdit::selectAll); 0458 0459 act = m_actions[ToggleBold] = new QAction(this); 0460 act->setIcon(QIcon::fromTheme("format-text-bold")); 0461 act->setText(i18nc("@action:inmenu Toggle bold style", "Bold")); 0462 act->setCheckable(true); 0463 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_BOLD); 0464 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontBold); 0465 0466 act = m_actions[ToggleItalic] = new QAction(this); 0467 act->setIcon(QIcon::fromTheme("format-text-italic")); 0468 act->setText(i18nc("@action:inmenu Toggle italic style", "Italic")); 0469 act->setCheckable(true); 0470 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_ITALIC); 0471 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontItalic); 0472 0473 act = m_actions[ToggleUnderline] = new QAction(this); 0474 act->setIcon(QIcon::fromTheme("format-text-underline")); 0475 act->setText(i18nc("@action:inmenu Toggle underline style", "Underline")); 0476 act->setCheckable(true); 0477 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_UNDERLINE); 0478 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontUnderline); 0479 0480 act = m_actions[ToggleStrikeOut] = new QAction(this); 0481 act->setIcon(QIcon::fromTheme("format-text-strikethrough")); 0482 act->setText(i18nc("@action:inmenu Toggle strike through style", "Strike Through")); 0483 act->setCheckable(true); 0484 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH); 0485 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleFontStrikeOut); 0486 0487 act = m_actions[ChangeTextColor] = new QAction(this); 0488 act->setIcon(QIcon::fromTheme("format-text-color")); 0489 act->setText(i18nc("@action:inmenu Change Text Color", "Text Color")); 0490 setupActionCommon(act, ACT_CHANGE_SELECTED_LINES_TEXT_COLOR); 0491 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::changeTextColor); 0492 0493 act = m_actions[CheckSpelling] = new QAction(this); 0494 act->setIcon(QIcon::fromTheme("tools-check-spelling")); 0495 act->setText(i18n("Check Spelling...")); 0496 connect(act, &QAction::triggered, app(), &Application::spellCheck); 0497 connect(act, &QAction::triggered, this, &KTextEdit::checkSpelling); 0498 0499 act = m_actions[ToggleAutoSpellChecking] = new QAction(this); 0500 act->setText(i18n("Auto Spell Check")); 0501 act->setCheckable(true); 0502 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleAutoSpellChecking); 0503 0504 act = m_actions[AllowTabulations] = new QAction(this); 0505 act->setText(i18n("Allow Tabulations")); 0506 connect(act, &QAction::triggered, this, &SimpleRichTextEdit::toggleTabChangesFocus); 0507 } 0508