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"