File indexing completed on 2024-12-08 10:25:49
0001 /* 0002 SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "inputtextmanager.h" 0008 #include "model/commandsmodelfilterproxymodel.h" 0009 #include "model/emoticonfilterproxymodel.h" 0010 #include "ownuser/ownuserpreferences.h" 0011 #include "rocketchataccount.h" 0012 #include "ruqola_completion_debug.h" 0013 0014 InputTextManager::InputTextManager(RocketChatAccount *account, QObject *parent) 0015 : QObject(parent) 0016 , mInputCompleterModel(new InputCompleterModel(account, this)) 0017 , mEmoticonFilterProxyModel(new EmoticonFilterProxyModel(this)) 0018 , mCommandFilterProxyModel(new CommandsModelFilterProxyModel(account, this)) 0019 , mRocketChatAccount(account) 0020 { 0021 } 0022 0023 InputTextManager::~InputTextManager() = default; 0024 0025 void InputTextManager::setEmoticonModel(QAbstractItemModel *model) 0026 { 0027 mEmoticonFilterProxyModel->setSourceModel(model); 0028 } 0029 0030 void InputTextManager::setCommandModel(QAbstractItemModel *model) 0031 { 0032 mCommandFilterProxyModel->setSourceModel(model); 0033 } 0034 0035 QAbstractItemModel *InputTextManager::commandModel() const 0036 { 0037 return mCommandFilterProxyModel; 0038 } 0039 0040 QString InputTextManager::applyCompletion(const QString &newWord, const QString &text, int *pPosition) 0041 { 0042 if (newWord.isEmpty()) { 0043 qCDebug(RUQOLA_COMPLETION_LOG) << "Empty newWord"; 0044 return text; 0045 } 0046 if (text.isEmpty()) { 0047 qCDebug(RUQOLA_COMPLETION_LOG) << "Empty text"; 0048 return text; 0049 } 0050 int position = *pPosition; 0051 // Cursor position can be at the end of word => text.length 0052 if ((position > text.length()) || (position < 0)) { 0053 qCDebug(RUQOLA_COMPLETION_LOG) << "Invalid position" << position; 0054 return text; 0055 } 0056 0057 int start = 0; 0058 for (int i = position - 1; i >= 0; i--) { 0059 if (i == 0) { 0060 start = 1; 0061 break; 0062 } 0063 if (text.at(i).isSpace()) { 0064 // Don't replace # or @ 0065 start = i + 2; 0066 break; 0067 } 0068 } 0069 int end = text.length() - 1; 0070 for (int i = position; i < text.length(); i++) { 0071 if (text.at(i).isSpace()) { 0072 end = i; 0073 if (!newWord.endsWith(QLatin1Char(' '))) { 0074 --end; 0075 } 0076 break; 0077 } 0078 } 0079 QString replaceText = text; 0080 const int textReplaceSize = end - start + 1; 0081 if (textReplaceSize > 0) { 0082 replaceText.replace(start, textReplaceSize, newWord); 0083 *pPosition = start + newWord.length(); 0084 } else if (textReplaceSize == 0) { 0085 replaceText += newWord; 0086 *pPosition = start + newWord.length(); 0087 } 0088 return replaceText; 0089 } 0090 0091 QString InputTextManager::searchWord(const QString &text, int position, int &start) 0092 { 0093 if (text.isEmpty()) { 0094 return {}; 0095 } 0096 // Cursor position can be at the end of word => text.length 0097 if ((position > text.length()) || (position < 0)) { 0098 qCWarning(RUQOLA_COMPLETION_LOG) << "Invalid position" << position << " text " << text; 0099 return {}; 0100 } 0101 0102 start = 0; 0103 for (int i = position; i > 0; i--) { 0104 if (text.at(i - 1).isSpace()) { 0105 start = i; 0106 break; 0107 } 0108 } 0109 int end = text.length() - 1; 0110 for (int i = position; i < text.length(); i++) { 0111 if (text.at(i).isSpace()) { 0112 end = i; 0113 break; 0114 } 0115 } 0116 0117 QString word = text.mid(start, end - start + 1); 0118 if (!word.isEmpty() && word.at(word.length() - 1).isSpace()) { 0119 word.chop(1); 0120 } 0121 // qDebug() << "position" << position << " word " << word << " text " << text << " start " << start << " end " << end; 0122 return word; 0123 } 0124 0125 void InputTextManager::setCompletionType(InputTextManager::CompletionForType type) 0126 { 0127 if (type != mCurrentCompletionType) { 0128 mCurrentCompletionType = type; 0129 Q_EMIT completionTypeChanged(type); 0130 } 0131 } 0132 0133 void InputTextManager::setInputTextChanged(const QString &roomId, const QString &text, int position) 0134 { 0135 if (text.isEmpty()) { 0136 clearCompleter(); 0137 return; 0138 } 0139 qCDebug(RUQOLA_COMPLETION_LOG) << "calling searchWord(" << text << "," << position << ")"; 0140 int start = -1; 0141 const QString word = searchWord(text, position, start); 0142 const QString str = word.mid(1); 0143 qCDebug(RUQOLA_COMPLETION_LOG) << " str:" << str << "start:" << start << "word:" << word << "position:" << position; 0144 if (word.isEmpty() || position != start + word.length()) { // only trigger completion at the end of the word 0145 clearCompleter(); 0146 } else { 0147 if (word.startsWith(QLatin1Char('@'))) { 0148 // Trigger autocompletion request in DDPClient (via RocketChatAccount) 0149 setCompletionType(InputTextManager::CompletionForType::User); 0150 mCurrentCompletionPattern = str; 0151 if (str.isEmpty()) { 0152 mInputCompleterModel->setDefaultUserCompletion(); 0153 } else { 0154 InputCompleterModel::SearchInfo searchInfo; 0155 searchInfo.searchString = str; 0156 searchInfo.searchType = InputCompleterModel::SearchInfo::SearchType::Users; 0157 mInputCompleterModel->setSearchInfo(std::move(searchInfo)); // necessary for make sure to show @here or @all 0158 Q_EMIT completionRequested(roomId, str, QString(), InputTextManager::CompletionForType::User); 0159 } 0160 } else if (word.startsWith(QLatin1Char('#'))) { 0161 // Trigger autocompletion request in DDPClient (via RocketChatAccount) 0162 mCurrentCompletionPattern = str; 0163 InputCompleterModel::SearchInfo searchInfo; 0164 searchInfo.searchType = InputCompleterModel::SearchInfo::SearchType::Channels; 0165 searchInfo.searchString = str; 0166 mInputCompleterModel->setSearchInfo(std::move(searchInfo)); 0167 Q_EMIT completionRequested(roomId, str, QString(), InputTextManager::CompletionForType::Channel); 0168 // slotCompletionChannels(str); 0169 setCompletionType(InputTextManager::CompletionForType::Channel); 0170 } else if (word.startsWith(QLatin1Char(':'))) { 0171 if (mRocketChatAccount && mRocketChatAccount->ownUserPreferences().useEmojis()) { 0172 mEmoticonFilterProxyModel->setFilterFixedString(word); 0173 setCompletionType(InputTextManager::CompletionForType::Emoji); 0174 } 0175 } else if (word.startsWith(QLatin1Char('/')) && position == word.length()) { // "/" must be at beginning of text 0176 mCommandFilterProxyModel->setRoomId(roomId); 0177 mCommandFilterProxyModel->setFilterFixedString(word); 0178 setCompletionType(InputTextManager::CompletionForType::Command); 0179 } else { 0180 clearCompleter(); 0181 } 0182 } 0183 } 0184 0185 void InputTextManager::clearCompleter() 0186 { 0187 mInputCompleterModel->clear(); 0188 setCompletionType(None); 0189 } 0190 0191 // Used by MessageTextEdit to set the completion model for the listview 0192 InputCompleterModel *InputTextManager::inputCompleterModel() const 0193 { 0194 return mInputCompleterModel; 0195 } 0196 0197 QAbstractItemModel *InputTextManager::emojiCompleterModel() const 0198 { 0199 return mEmoticonFilterProxyModel; 0200 } 0201 0202 // Called by DDPClient to fill in the completer model based on the typed input 0203 void InputTextManager::inputTextCompleter(const QJsonObject &obj) 0204 { 0205 if (mCurrentCompletionType == None) { 0206 return; 0207 } 0208 mInputCompleterModel->parseChannels(obj); 0209 // Don't show a popup with exactly the same as the pattern 0210 // (e.g. type or navigate within @dfaure -> the offer is "dfaure", useless) 0211 if (mInputCompleterModel->rowCount() == 1) { 0212 const QString completerName = mInputCompleterModel->index(0, 0).data(InputCompleterModel::CompleterName).toString(); 0213 if (mCurrentCompletionPattern == completerName || mCurrentCompletionPattern.isEmpty()) { 0214 clearCompleter(); 0215 return; 0216 } 0217 } 0218 Q_EMIT selectFirstTextCompleter(); 0219 } 0220 0221 #include "moc_inputtextmanager.cpp"