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"