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