// SPDX-FileCopyrightText: 2022 Tobias Fella // SPDX-License-Identifier: LGPL-2.0-or-later #include "completionmodel.h" #include #include #include "chattextitemhelper.h" #include "completionproxymodel.h" #include "models/actionsmodel.h" #include "models/customemojimodel.h" #include "models/emojimodel.h" #include "models/roomlistmodel.h" #include "userlistmodel.h" CompletionModel::CompletionModel(QObject *parent) : QAbstractListModel(parent) , m_textItem(new ChatTextItemHelper(this)) , m_filterModel(new CompletionProxyModel(this)) , m_emojiModel(new QConcatenateTablesProxyModel(this)) { m_emojiModel->addSourceModel(&CustomEmojiModel::instance()); m_emojiModel->addSourceModel(&EmojiModel::instance()); } NeoChatRoom *CompletionModel::room() const { return m_room; } void CompletionModel::setRoom(NeoChatRoom *room) { if (m_room == room) { return; } m_room = room; Q_EMIT roomChanged(); } ChatBarType::Type CompletionModel::type() const { return m_type; } void CompletionModel::setType(ChatBarType::Type type) { if (type == m_type) { return; } m_type = type; Q_EMIT typeChanged(); } ChatTextItemHelper *CompletionModel::textItem() const { return m_textItem; } void CompletionModel::setTextItem(ChatTextItemHelper *textItem) { if (textItem == m_textItem) { return; } if (m_textItem) { m_textItem->disconnect(this); } m_textItem = textItem; if (m_textItem) { connect(m_textItem, &ChatTextItemHelper::cursorPositionChanged, this, &CompletionModel::updateTextStart); connect(m_textItem, &ChatTextItemHelper::contentsChanged, this, &CompletionModel::updateCompletion); } Q_EMIT textItemChanged(); } bool CompletionModel::isCompleting() const { if (!m_textItem) { return false; } return m_textItem->isCompleting; } void CompletionModel::ignoreCurrentCompletion() { m_ignoreCurrentCompletion = true; m_textItem->isCompleting = false; Q_EMIT isCompletingChanged(); } void CompletionModel::updateTextStart() { auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); while (cursor.selectedText() != u' ' && !cursor.atBlockStart()) { cursor.movePosition(QTextCursor::PreviousCharacter); cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); } m_textStart = cursor.position() == 0 && cursor.selectedText() != u' ' ? 0 : cursor.position() + 1; updateCompletion(); } int CompletionModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); if (m_autoCompletionType == None) { return 0; } return m_filterModel->rowCount(); } QVariant CompletionModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_filterModel->rowCount()) { return {}; } auto filterIndex = m_filterModel->index(index.row(), 0); if (m_autoCompletionType == User) { if (role == DisplayNameRole) { return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole); } if (role == SubtitleRole) { return m_filterModel->data(filterIndex, UserListModel::UserIdRole); } if (role == IconNameRole) { return m_filterModel->data(filterIndex, UserListModel::AvatarRole); } if (role == ReplacedTextRole) { return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole); } if (role == HRefRole) { return u"https://matrix.to/#/%1"_s.arg(m_filterModel->data(filterIndex, UserListModel::UserIdRole).toString()); } } if (m_autoCompletionType == Command) { if (role == DisplayNameRole) { return u"%1 %2"_s.arg(m_filterModel->data(filterIndex, ActionsModel::Prefix).toString(), m_filterModel->data(filterIndex, ActionsModel::Parameters).toString()); } if (role == SubtitleRole) { return m_filterModel->data(filterIndex, ActionsModel::Description); } if (role == IconNameRole) { return u"invalid"_s; } if (role == ReplacedTextRole) { return m_filterModel->data(filterIndex, ActionsModel::Prefix); } } if (m_autoCompletionType == Room) { if (role == DisplayNameRole) { return m_filterModel->data(filterIndex, RoomListModel::DisplayNameRole); } if (role == SubtitleRole) { return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole); } if (role == IconNameRole) { return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString(); } if (role == ReplacedTextRole) { return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole); } if (role == HRefRole) { return u"https://matrix.to/#/%1"_s.arg(m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole).toString()); } } if (m_autoCompletionType == Emoji) { if (role == DisplayNameRole) { return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole); } if (role == IconNameRole) { return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl); } if (role == ReplacedTextRole) { return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole); } if (role == SubtitleRole) { return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole); } } return {}; } QHash CompletionModel::roleNames() const { return { {DisplayNameRole, "displayName"}, {SubtitleRole, "subtitle"}, {IconNameRole, "iconName"}, {ReplacedTextRole, "replacedText"}, {HRefRole, "hRef"}, }; } void CompletionModel::updateCompletion() { auto cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } if (m_ignoreCurrentCompletion) { cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); if (cursor.selectedText() == u' ') { m_ignoreCurrentCompletion = false; } return; } cursor.setPosition(m_textStart); while (!cursor.selectedText().endsWith(u' ') && !cursor.atBlockEnd()) { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); } const auto text = cursor.selectedText().trimmed(); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); const auto fullText = cursor.selectedText(); if (text.startsWith(QLatin1Char('@'))) { m_filterModel->setSourceModel(m_userListModel); m_filterModel->setFilterRole(UserListModel::UserIdRole); m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole); m_filterModel->setFullText(fullText); m_filterModel->setFilterText(text); m_autoCompletionType = User; m_filterModel->invalidate(); } else if (text.startsWith(QLatin1Char('/'))) { m_filterModel->setSourceModel(&ActionsModel::instance()); m_filterModel->setFilterRole(ActionsModel::Prefix); m_filterModel->setSecondaryFilterRole(-1); m_filterModel->setFullText(fullText); m_filterModel->setFilterText(text.mid(1)); m_autoCompletionType = Command; m_filterModel->invalidate(); } else if (text.startsWith(QLatin1Char('#'))) { m_autoCompletionType = Room; m_filterModel->setSourceModel(m_roomListModel); m_filterModel->setFilterRole(RoomListModel::CanonicalAliasRole); m_filterModel->setSecondaryFilterRole(RoomListModel::DisplayNameRole); m_filterModel->setFullText(fullText); m_filterModel->setFilterText(text); m_filterModel->invalidate(); } else if (text.startsWith(QLatin1Char(':')) && text.size() > 1 && !text[1].isUpper() && (fullText.indexOf(QLatin1Char(':'), 1) == -1 || (fullText.indexOf(QLatin1Char(' ')) != -1 && fullText.indexOf(QLatin1Char(':'), 1) > fullText.indexOf(QLatin1Char(' '), 1)))) { m_filterModel->setSourceModel(m_emojiModel); m_autoCompletionType = Emoji; m_filterModel->setFilterRole(CustomEmojiModel::Name); m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole); m_filterModel->setFullText(fullText); m_filterModel->setFilterText(text); m_filterModel->invalidate(); } else { m_autoCompletionType = None; } beginResetModel(); endResetModel(); m_textItem->isCompleting = rowCount() > 0; Q_EMIT isCompletingChanged(); } CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const { return m_autoCompletionType; } void CompletionModel::setAutoCompletionType(AutoCompletionType autoCompletionType) { m_autoCompletionType = autoCompletionType; Q_EMIT autoCompletionTypeChanged(); } RoomListModel *CompletionModel::roomListModel() const { return m_roomListModel; } void CompletionModel::setRoomListModel(RoomListModel *roomListModel) { m_roomListModel = roomListModel; Q_EMIT roomListModelChanged(); } UserListModel *CompletionModel::userListModel() const { return m_userListModel; } void CompletionModel::setUserListModel(UserListModel *userListModel) { if (userListModel == m_userListModel) { return; } m_userListModel = userListModel; Q_EMIT userListModelChanged(); } void CompletionModel::insertCompletion(const QString &text, const QUrl &link) { QTextCursor cursor = m_textItem->textCursor(); if (cursor.isNull()) { return; } cursor.beginEditBlock(); while (!cursor.selectedText().startsWith(u' ') && !cursor.atBlockStart()) { cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); } if (cursor.selectedText().startsWith(u' ')) { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); } cursor.removeSelectedText(); const int start = cursor.position(); const auto insertString = u"%1 %2"_s.arg(text, link.isEmpty() ? QString() : u" "_s); cursor.insertText(insertString); cursor.setPosition(start); cursor.setPosition(start + text.size(), QTextCursor::KeepAnchor); cursor.setKeepPositionOnInsert(true); cursor.endEditBlock(); if (!link.isEmpty()) { pushMention({ .cursor = cursor, .text = text, .id = link.toString(), }); } m_textItem->rehighlight(); } void CompletionModel::pushMention(const Mention mention) const { if (!m_room || m_type == ChatBarType::None) { return; } m_room->cacheForType(m_type)->mentions()->push_back(mention); } #include "moc_completionmodel.cpp"