File indexing completed on 2024-12-01 04:37:01
0001 /* 0002 SPDX-FileCopyrightText: 2020-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "messagetextedit.h" 0008 #include "common/commandcompletiondelegate.h" 0009 #include "common/completionlistview.h" 0010 #include "common/emojicompletiondelegate.h" 0011 #include "common/userandchannelcompletiondelegate.h" 0012 #include "model/inputcompletermodel.h" 0013 #include "rocketchataccount.h" 0014 #include "ruqola.h" 0015 #include <KLocalizedString> 0016 0017 #include <KConfigGroup> 0018 #include <KSharedConfig> 0019 #include <QAbstractTextDocumentLayout> 0020 #include <QApplication> 0021 #include <QClipboard> 0022 #include <QKeyEvent> 0023 #include <QMenu> 0024 #include <QMimeData> 0025 #include <QTextCursor> 0026 #include <QTextDocument> 0027 0028 #include "config-ruqola.h" 0029 0030 #if HAVE_TEXT_AUTOCORRECTION_WIDGETS 0031 #include "textautocorrectioncore/textautocorrectionsettings.h" 0032 #include <TextAutoCorrectionCore/AutoCorrection> 0033 #endif 0034 0035 MessageTextEdit::MessageTextEdit(QWidget *parent) 0036 : KTextEdit(parent) 0037 , mUserAndChannelCompletionListView(new CompletionListView) 0038 , mEmojiCompletionListView(new CompletionListView) 0039 , mCommandCompletionListView(new CompletionListView) 0040 { 0041 setAcceptRichText(false); 0042 enableFindReplace(false); // not needed here, let's instead make sure the Ctrl+F shortcut will search through channel history 0043 0044 connect(document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, this, &QWidget::updateGeometry); 0045 0046 mUserAndChannelCompletionDelegate = new UserAndChannelCompletionDelegate(mUserAndChannelCompletionListView); 0047 mUserAndChannelCompletionListView->setItemDelegate(mUserAndChannelCompletionDelegate); 0048 mUserAndChannelCompletionListView->setTextWidget(this); 0049 connect(mUserAndChannelCompletionListView, &CompletionListView::complete, this, &MessageTextEdit::slotComplete); 0050 0051 mEmojiCompletionListView->setItemDelegate(new EmojiCompletionDelegate(mEmojiCompletionListView)); 0052 mEmojiCompletionListView->setTextWidget(this); 0053 connect(mEmojiCompletionListView, &CompletionListView::complete, this, &MessageTextEdit::slotComplete); 0054 0055 mCommandCompletionListView->setItemDelegate(new CommandCompletionDelegate(mCommandCompletionListView)); 0056 mCommandCompletionListView->setTextWidget(this); 0057 connect(mCommandCompletionListView, &CompletionListView::complete, this, &MessageTextEdit::slotComplete); 0058 loadSpellCheckingSettings(); 0059 connect(this, &MessageTextEdit::languageChanged, this, &MessageTextEdit::slotLanguageChanged); 0060 connect(this, &MessageTextEdit::checkSpellingChanged, this, &MessageTextEdit::slotSpellCheckingEnableChanged); 0061 } 0062 0063 MessageTextEdit::~MessageTextEdit() 0064 { 0065 delete mUserAndChannelCompletionListView; 0066 delete mEmojiCompletionListView; 0067 delete mCommandCompletionListView; 0068 } 0069 0070 void MessageTextEdit::slotSpellCheckingEnableChanged(bool b) 0071 { 0072 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0073 KConfigGroup group(config, QStringLiteral("Spelling")); 0074 group.writeEntry("checkerEnabledByDefault", b); 0075 } 0076 0077 void MessageTextEdit::slotLanguageChanged(const QString &lang) 0078 { 0079 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0080 KConfigGroup group(config, QStringLiteral("Spelling")); 0081 group.writeEntry("Language", lang); 0082 switchAutoCorrectionLanguage(lang); 0083 } 0084 0085 void MessageTextEdit::switchAutoCorrectionLanguage(const QString &lang) 0086 { 0087 #if HAVE_TEXT_AUTOCORRECTION_WIDGETS 0088 if (!lang.isEmpty()) { 0089 auto settings = Ruqola::self()->autoCorrection()->autoCorrectionSettings(); 0090 settings->setLanguage(lang); 0091 Ruqola::self()->autoCorrection()->setAutoCorrectionSettings(settings); 0092 } 0093 #endif 0094 qDebug() << " MessageTextEdit::switchAutoCorrectionLanguage " << lang; 0095 } 0096 0097 void MessageTextEdit::loadSpellCheckingSettings() 0098 { 0099 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0100 if (config->hasGroup(QLatin1String("Spelling"))) { 0101 KConfigGroup group(config, QStringLiteral("Spelling")); 0102 setCheckSpellingEnabled(group.readEntry("checkerEnabledByDefault", false)); 0103 const QString language = group.readEntry("Language", QString()); 0104 setSpellCheckingLanguage(language); 0105 switchAutoCorrectionLanguage(language); 0106 } 0107 } 0108 0109 void MessageTextEdit::setCurrentRocketChatAccount(RocketChatAccount *account, bool threadMessageDialog) 0110 { 0111 if (mCurrentInputTextManager) { 0112 disconnect(mCurrentInputTextManager, &InputTextManager::completionTypeChanged, this, &MessageTextEdit::slotCompletionTypeChanged); 0113 disconnect(mCurrentInputTextManager, &InputTextManager::selectFirstTextCompleter, this, &MessageTextEdit::slotSelectFirstTextCompleter); 0114 disconnect(mCurrentRocketChatAccount, &RocketChatAccount::loginStatusChanged, this, &MessageTextEdit::slotLoginChanged); 0115 } 0116 mCurrentRocketChatAccount = account; 0117 mCurrentInputTextManager = threadMessageDialog ? mCurrentRocketChatAccount->inputThreadMessageTextManager() : mCurrentRocketChatAccount->inputTextManager(); 0118 mUserAndChannelCompletionListView->setModel(mCurrentInputTextManager->inputCompleterModel()); 0119 mEmojiCompletionListView->setModel(mCurrentInputTextManager->emojiCompleterModel()); 0120 mCommandCompletionListView->setModel(mCurrentInputTextManager->commandModel()); 0121 mUserAndChannelCompletionDelegate->setRocketChatAccount(mCurrentRocketChatAccount); 0122 connect(mCurrentInputTextManager, &InputTextManager::completionTypeChanged, this, &MessageTextEdit::slotCompletionTypeChanged); 0123 connect(mCurrentInputTextManager, &InputTextManager::selectFirstTextCompleter, this, &MessageTextEdit::slotSelectFirstTextCompleter); 0124 connect(mCurrentRocketChatAccount, &RocketChatAccount::loginStatusChanged, this, &MessageTextEdit::slotLoginChanged); 0125 connect(mCurrentRocketChatAccount, &RocketChatAccount::updateMessageFailed, this, &MessageTextEdit::slotUpdateMessageFailed); 0126 } 0127 0128 void MessageTextEdit::slotUpdateMessageFailed(const QString &str) 0129 { 0130 // Message Update failed => don't lose text! 0131 setText(str); 0132 } 0133 0134 void MessageTextEdit::slotLoginChanged() 0135 { 0136 const auto loginStatus = mCurrentRocketChatAccount->loginStatus(); 0137 if (loginStatus != DDPAuthenticationManager::LoggedIn) { 0138 mUserAndChannelCompletionListView->hide(); 0139 mEmojiCompletionListView->hide(); 0140 mCommandCompletionListView->hide(); 0141 } 0142 } 0143 0144 void MessageTextEdit::insertEmoji(const QString &text) 0145 { 0146 textCursor().insertText(text + QLatin1Char(' ')); 0147 Q_EMIT textEditing(false); 0148 } 0149 0150 QString MessageTextEdit::text() const 0151 { 0152 return toPlainText(); 0153 } 0154 0155 QSize MessageTextEdit::sizeHint() const 0156 { 0157 // The width of the QTextDocument is the current widget width, so this is somewhat circular logic. 0158 // But I don't really want to redo the layout with a different width like idealWidth(), seems slow. 0159 const QSize docSize = document()->size().toSize(); 0160 const int margin = int(document()->documentMargin()); 0161 return {docSize.width() + margin, qMin(300, docSize.height()) + margin}; 0162 } 0163 0164 QSize MessageTextEdit::minimumSizeHint() const 0165 { 0166 const int margin = int(document()->documentMargin()); 0167 return {300, fontMetrics().height() + margin}; 0168 } 0169 0170 void MessageTextEdit::changeText(const QString &newText, int cursorPosition) 0171 { 0172 setPlainText(newText); 0173 0174 QTextCursor cursor(document()); 0175 cursor.setPosition(cursorPosition); 0176 setTextCursor(cursor); 0177 0178 mCurrentInputTextManager->setInputTextChanged(roomId(), text(), cursorPosition); 0179 } 0180 0181 QMenu *MessageTextEdit::mousePopupMenu() 0182 { 0183 QMenu *menu = KTextEdit::mousePopupMenu(); 0184 0185 QClipboard *clip = QApplication::clipboard(); 0186 const QMimeData *mimeData = clip->mimeData(); 0187 if (mimeData->hasImage()) { 0188 menu->addSeparator(); 0189 menu->addAction(i18n("Paste Image"), this, [this, mimeData]() { 0190 Q_EMIT handleMimeData(mimeData); 0191 }); 0192 } 0193 menu->addSeparator(); 0194 0195 auto formatMenu = new QMenu(menu); 0196 formatMenu->setTitle(i18n("Change Text Format")); 0197 menu->addMenu(formatMenu); 0198 formatMenu->addAction(QIcon::fromTheme(QStringLiteral("format-text-bold")), i18n("Bold"), this, &MessageTextEdit::slotSetAsBold); 0199 formatMenu->addAction(QIcon::fromTheme(QStringLiteral("format-text-italic")), i18n("Italic"), this, &MessageTextEdit::slotSetAsItalic); 0200 formatMenu->addAction(QIcon::fromTheme(QStringLiteral("format-text-strikethrough")), i18n("Strike-out"), this, &MessageTextEdit::slotSetAsStrikeOut); 0201 formatMenu->addSeparator(); 0202 formatMenu->addAction(i18n("Code Block"), this, &MessageTextEdit::slotInsertCodeBlock); 0203 formatMenu->addSeparator(); 0204 formatMenu->addAction(i18n("Markdown Url"), this, &MessageTextEdit::slotInsertMarkdownUrl); 0205 return menu; 0206 } 0207 0208 void MessageTextEdit::setRoomId(const QString &roomId) 0209 { 0210 mRoomId = roomId; 0211 } 0212 0213 QString MessageTextEdit::roomId() const 0214 { 0215 return mRoomId; 0216 } 0217 0218 void MessageTextEdit::slotInsertMarkdownUrl() 0219 { 0220 QTextCursor cursor = textCursor(); 0221 if (cursor.hasSelection()) { 0222 const QString mardownUrlStr{QStringLiteral("[text](%1)").arg(cursor.selectedText())}; 0223 cursor.insertText(mardownUrlStr); 0224 cursor.setPosition(cursor.position() - mardownUrlStr.length() + 1); 0225 } else { 0226 const QString mardownUrlStr{QStringLiteral("[text](url)")}; 0227 cursor.insertText(mardownUrlStr); 0228 cursor.setPosition(cursor.position() - mardownUrlStr.length() + 1); 0229 } 0230 setTextCursor(cursor); 0231 } 0232 0233 void MessageTextEdit::slotInsertCodeBlock() 0234 { 0235 const QString textCodeBlock{QStringLiteral("```")}; 0236 QTextCursor cursor = textCursor(); 0237 if (cursor.hasSelection()) { 0238 const QString text = textCodeBlock + QLatin1Char('\n') + cursor.selectedText() + QLatin1Char('\n') + textCodeBlock; 0239 cursor.insertText(text); 0240 } else { 0241 cursor.insertText(QString(textCodeBlock + QStringLiteral("\n\n") + textCodeBlock)); 0242 } 0243 cursor.setPosition(cursor.position() - 4); 0244 setTextCursor(cursor); 0245 } 0246 0247 void MessageTextEdit::slotSetAsStrikeOut() 0248 { 0249 insertFormat(QLatin1Char('~')); 0250 } 0251 0252 void MessageTextEdit::slotSetAsBold() 0253 { 0254 insertFormat(QLatin1Char('*')); 0255 } 0256 0257 void MessageTextEdit::slotSetAsItalic() 0258 { 0259 insertFormat(QLatin1Char('_')); 0260 } 0261 0262 void MessageTextEdit::insertFormat(QChar formatChar) 0263 { 0264 QTextCursor cursor = textCursor(); 0265 if (cursor.hasSelection()) { 0266 const QString text = formatChar + cursor.selectedText() + formatChar; 0267 cursor.insertText(text); 0268 } else { 0269 cursor.insertText(QString(formatChar) + QString(formatChar)); 0270 } 0271 cursor.setPosition(cursor.position() - 1); 0272 setTextCursor(cursor); 0273 } 0274 0275 void MessageTextEdit::keyPressEvent(QKeyEvent *e) 0276 { 0277 const int key = e->key(); 0278 0279 #if HAVE_TEXT_AUTOCORRECTION_WIDGETS 0280 if (Ruqola::self()->autoCorrection()->autoCorrectionSettings()->isEnabledAutoCorrection()) { 0281 if ((key == Qt::Key_Space) || (key == Qt::Key_Enter) || (key == Qt::Key_Return)) { 0282 if (!textCursor().hasSelection()) { 0283 int position = textCursor().position(); 0284 // no Html format in subject. => false 0285 const bool addSpace = Ruqola::self()->autoCorrection()->autocorrect(false, *document(), position); 0286 QTextCursor cur = textCursor(); 0287 cur.setPosition(position); 0288 if (key == Qt::Key_Space) { 0289 if (addSpace) { 0290 cur.insertText(QStringLiteral(" ")); 0291 setTextCursor(cur); 0292 } 0293 return; 0294 } 0295 } 0296 } 0297 } 0298 #endif 0299 0300 if (key == Qt::Key_Return || key == Qt::Key_Enter) { 0301 if ((key == Qt::Key_Enter && (e->modifiers() == Qt::KeypadModifier)) || !e->modifiers()) { 0302 Q_EMIT sendMessage(text()); 0303 // We send text => we will clear => we will send textEditing is empty => clear notification 0304 Q_EMIT textEditing(true); 0305 clear(); 0306 } else { 0307 textCursor().insertBlock(); 0308 ensureCursorVisible(); 0309 } 0310 e->accept(); 0311 return; 0312 } else if (key == Qt::Key_Up || key == Qt::Key_Down) { 0313 if (!(e->modifiers() & Qt::AltModifier)) { 0314 // document()->lineCount() is > 1 if the user used Shift+Enter 0315 // firstBlockLayout->lineCount() is > 1 if a single long line wrapped around 0316 const QTextLayout *firstBlockLayout = document()->firstBlock().layout(); 0317 if (document()->lineCount() > 1 || firstBlockLayout->lineCount() > 1) { 0318 KTextEdit::keyPressEvent(e); 0319 return; 0320 } 0321 } 0322 } 0323 e->ignore(); 0324 // Check if the listview or room widget want to handle the key (e.g Esc, PageUp) 0325 Q_EMIT keyPressed(e); 0326 if (e->isAccepted()) { 0327 return; 0328 } 0329 // Assign key to KTextEdit first otherwise text() doesn't return correct text 0330 KTextEdit::keyPressEvent(e); 0331 if (key == Qt::Key_Delete || key == Qt::Key_Backspace) { 0332 if (textCursor().hasSelection() && textCursor().selectedText() == text()) { 0333 // We will clear all text => we will send textEditing is empty => clear notification 0334 Q_EMIT textEditing(true); 0335 } else { 0336 mCurrentInputTextManager->setInputTextChanged(roomId(), text(), textCursor().position()); 0337 Q_EMIT textEditing(document()->isEmpty()); 0338 } 0339 } else { 0340 if (!e->text().isEmpty() || e->matches(QKeySequence::Paste) || e->matches(QKeySequence::Redo) || e->matches(QKeySequence::Undo)) { 0341 mCurrentInputTextManager->setInputTextChanged(roomId(), text(), textCursor().position()); 0342 Q_EMIT textEditing(document()->isEmpty()); 0343 } 0344 } 0345 } 0346 0347 void MessageTextEdit::mousePressEvent(QMouseEvent *ev) 0348 { 0349 if (ev->buttons().testFlag(Qt::LeftButton)) { 0350 Q_EMIT textClicked(); 0351 } 0352 KTextEdit::mousePressEvent(ev); 0353 } 0354 0355 void MessageTextEdit::dragEnterEvent(QDragEnterEvent *event) 0356 { 0357 const QMimeData *mimeData = event->mimeData(); 0358 if (mimeData->hasUrls()) { 0359 event->accept(); 0360 // Don't let QTextEdit move the cursor around 0361 return; 0362 } 0363 KTextEdit::dragEnterEvent(event); 0364 } 0365 0366 void MessageTextEdit::dragMoveEvent(QDragMoveEvent *event) 0367 { 0368 const QMimeData *mimeData = event->mimeData(); 0369 if (mimeData->hasUrls()) { 0370 event->accept(); 0371 // Don't let QTextEdit move the cursor around 0372 return; 0373 } 0374 KTextEdit::dragMoveEvent(event); 0375 } 0376 0377 void MessageTextEdit::dropEvent(QDropEvent *event) 0378 { 0379 const QMimeData *mimeData = event->mimeData(); 0380 if (mimeData->hasUrls()) { 0381 Q_EMIT handleMimeData(mimeData); 0382 event->accept(); 0383 return; 0384 } 0385 KTextEdit::dropEvent(event); 0386 } 0387 0388 void MessageTextEdit::slotCompletionTypeChanged(InputTextManager::CompletionForType type) 0389 { 0390 if (type == InputTextManager::Emoji) { 0391 // show emoji completion popup when typing ':' 0392 mEmojiCompletionListView->slotCompletionAvailable(); 0393 mUserAndChannelCompletionListView->hide(); 0394 mCommandCompletionListView->hide(); 0395 } else if (type == InputTextManager::None) { 0396 mUserAndChannelCompletionListView->hide(); 0397 mEmojiCompletionListView->hide(); 0398 mCommandCompletionListView->hide(); 0399 } else if (type == InputTextManager::Command) { 0400 mUserAndChannelCompletionListView->hide(); 0401 mEmojiCompletionListView->hide(); 0402 mCommandCompletionListView->slotCompletionAvailable(); 0403 } else { 0404 // the user and channel completion inserts rows when typing '@' so it will trigger slotCompletionAvailable automatically 0405 mEmojiCompletionListView->hide(); 0406 mCommandCompletionListView->hide(); 0407 } 0408 } 0409 0410 void MessageTextEdit::slotComplete(const QModelIndex &index) 0411 { 0412 const QString completerName = index.data(InputCompleterModel::CompleterName).toString(); 0413 if (completerName.isEmpty()) { 0414 return; 0415 } 0416 int textPos = textCursor().position(); 0417 const QString newText = mCurrentInputTextManager->applyCompletion(completerName + QLatin1Char(' '), text(), &textPos); 0418 0419 mUserAndChannelCompletionListView->hide(); 0420 mEmojiCompletionListView->hide(); 0421 mCommandCompletionListView->hide(); 0422 0423 changeText(newText, textPos); 0424 } 0425 0426 void MessageTextEdit::slotSelectFirstTextCompleter() 0427 { 0428 const QModelIndex idx = mUserAndChannelCompletionListView->model()->index(0, 0); 0429 // qDebug() << " idx " << idx; 0430 mUserAndChannelCompletionListView->setCurrentIndex(idx); 0431 } 0432 0433 #include "moc_messagetextedit.cpp"