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