summary refs log tree commit diff
path: root/src/TextInputWidget.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/TextInputWidget.cpp')
-rw-r--r--src/TextInputWidget.cpp496
1 files changed, 0 insertions, 496 deletions
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
deleted file mode 100644

index 6d1fdc7a..00000000 --- a/src/TextInputWidget.cpp +++ /dev/null
@@ -1,496 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QAbstractItemView> -#include <QAbstractTextDocumentLayout> -#include <QBuffer> -#include <QClipboard> -#include <QCompleter> -#include <QFileDialog> -#include <QMimeData> -#include <QMimeDatabase> -#include <QMimeType> -#include <QPainter> -#include <QStyleOption> -#include <QtConcurrent> - -#include "Cache.h" -#include "ChatPage.h" -#include "CompletionModelRoles.h" -#include "CompletionProxyModel.h" -#include "Logging.h" -#include "TextInputWidget.h" -#include "Utils.h" -#include "emoji/EmojiSearchModel.h" -#include "emoji/Provider.h" -#include "ui/FlatButton.h" -#include "ui/LoadingIndicator.h" - -#if defined(Q_OS_MAC) -#include "emoji/MacHelper.h" -#endif - -static constexpr size_t INPUT_HISTORY_SIZE = 127; -static constexpr int MAX_TEXTINPUT_HEIGHT = 120; -static constexpr int ButtonHeight = 22; - -FilteredTextEdit::FilteredTextEdit(QWidget *parent) - : QTextEdit{parent} - , history_index_{0} - , suggestionsPopup_{parent} - , previewDialog_{parent} -{ - setFrameStyle(QFrame::NoFrame); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - &FilteredTextEdit::updateGeometry); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - [this]() { emit heightChanged(document()->size().toSize().height()); }); - working_history_.push_back(""); - connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); - setAcceptRichText(false); - - completer_ = new QCompleter(this); - completer_->setWidget(this); - auto model = new emoji::EmojiSearchModel(this); - model->sort(0, Qt::AscendingOrder); - completer_->setModel((emoji_completion_model_ = new CompletionProxyModel(model, this))); - emoji_completion_model_->setFilterRole(CompletionModel::SearchRole); - completer_->setModelSorting(QCompleter::UnsortedModel); - completer_->popup()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - completer_->popup()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - connect(completer_, - QOverload<const QModelIndex &>::of(&QCompleter::activated), - [this](auto &index) { - emoji_popup_open_ = false; - auto text = index.data(CompletionModel::CompletionRole).toString(); - insertCompletion(text); - }); - - connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); - connect( - &suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { - suggestionsPopup_.hide(); - - auto cursor = textCursor(); - const int end = cursor.position(); - - cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); - cursor.setPosition(end, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - cursor.insertText(text); - }); - - // For cycling through the suggestions by hitting tab. - connect(this, - &FilteredTextEdit::selectNextSuggestion, - &suggestionsPopup_, - &SuggestionsPopup::selectNextSuggestion); - connect(this, - &FilteredTextEdit::selectPreviousSuggestion, - &suggestionsPopup_, - &SuggestionsPopup::selectPreviousSuggestion); - connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { - suggestionsPopup_.selectHoveredSuggestion<UserItem>(); - }); - - previewDialog_.hide(); -} - -void -FilteredTextEdit::insertCompletion(QString completion) -{ - // Paint the current word and replace it with 'completion' - auto cur_text = textAfterPosition(trigger_pos_); - auto tc = textCursor(); - tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cur_text.length()); - tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, cur_text.length()); - tc.insertText(completion); - setTextCursor(tc); -} - -void -FilteredTextEdit::showResults(const std::vector<SearchResult> &results) -{ - QPoint pos; - - if (isAnchorValid()) { - auto cursor = textCursor(); - cursor.setPosition(atTriggerPosition_); - pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft()); - } else { - auto rect = cursorRect(); - pos = viewport()->mapToGlobal(rect.topLeft()); - } - - suggestionsPopup_.addUsers(results); - suggestionsPopup_.move(pos.x(), pos.y() - suggestionsPopup_.height() - 10); - suggestionsPopup_.show(); -} - -void -FilteredTextEdit::keyPressEvent(QKeyEvent *event) -{ - const bool isModifier = (event->modifiers() != Qt::NoModifier); - -#if defined(Q_OS_MAC) - if (event->modifiers() == (Qt::ControlModifier | Qt::MetaModifier) && - event->key() == Qt::Key_Space) - MacHelper::showEmojiWindow(); -#endif - - if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_U) - QTextEdit::setText(""); - - // calculate the new query - if (textCursor().position() < atTriggerPosition_ || !isAnchorValid()) { - resetAnchor(); - closeSuggestions(); - } - - if (suggestionsPopup_.isVisible()) { - switch (event->key()) { - case Qt::Key_Down: - case Qt::Key_Tab: - emit selectNextSuggestion(); - return; - case Qt::Key_Enter: - case Qt::Key_Return: - emit selectHoveredSuggestion(); - return; - case Qt::Key_Escape: - closeSuggestions(); - return; - case Qt::Key_Up: - case Qt::Key_Backtab: { - emit selectPreviousSuggestion(); - return; - } - default: - break; - } - } - - if (emoji_popup_open_) { - auto fake_key = (event->key() == Qt::Key_Backtab) ? Qt::Key_Up : Qt::Key_Down; - switch (event->key()) { - case Qt::Key_Backtab: - case Qt::Key_Tab: { - // Simulate up/down arrow press - auto ev = new QKeyEvent(QEvent::KeyPress, fake_key, Qt::NoModifier); - QCoreApplication::postEvent(completer_->popup(), ev); - return; - } - default: - break; - } - } - - switch (event->key()) { - case Qt::Key_At: - atTriggerPosition_ = textCursor().position(); - anchorType_ = AnchorType::Sigil; - - QTextEdit::keyPressEvent(event); - break; - case Qt::Key_Tab: { - auto cursor = textCursor(); - const int initialPos = cursor.position(); - - cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); - auto word = cursor.selectedText(); - - const int startOfWord = cursor.position(); - - // There is a word to complete. - if (initialPos != startOfWord) { - atTriggerPosition_ = startOfWord; - anchorType_ = AnchorType::Tab; - - emit showSuggestions(word); - } else { - QTextEdit::keyPressEvent(event); - } - - break; - } - case Qt::Key_Colon: { - QTextEdit::keyPressEvent(event); - trigger_pos_ = textCursor().position() - 1; - emoji_completion_model_->setFilterRegExp(""); - emoji_popup_open_ = true; - break; - } - case Qt::Key_Return: - case Qt::Key_Enter: - if (emoji_popup_open_) { - if (!completer_->popup()->currentIndex().isValid()) { - // No completion to select, do normal behavior - completer_->popup()->hide(); - emoji_popup_open_ = false; - } else { - event->ignore(); - return; - } - } - - if (!(event->modifiers() & Qt::ShiftModifier)) { - submit(); - } else { - QTextEdit::keyPressEvent(event); - } - break; - case Qt::Key_Up: { - auto initial_cursor = textCursor(); - QTextEdit::keyPressEvent(event); - - if (textCursor() == initial_cursor && textCursor().atStart() && - history_index_ + 1 < working_history_.size()) { - ++history_index_; - setPlainText(working_history_[history_index_]); - moveCursor(QTextCursor::End); - } else if (textCursor() == initial_cursor) { - // Move to the start of the text if there aren't any lines to move up to. - initial_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1); - setTextCursor(initial_cursor); - } - - break; - } - case Qt::Key_Down: { - auto initial_cursor = textCursor(); - QTextEdit::keyPressEvent(event); - - if (textCursor() == initial_cursor && textCursor().atEnd() && history_index_ > 0) { - --history_index_; - setPlainText(working_history_[history_index_]); - moveCursor(QTextCursor::End); - } else if (textCursor() == initial_cursor) { - // Move to the end of the text if there aren't any lines to move down to. - initial_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor, 1); - setTextCursor(initial_cursor); - } - - break; - } - default: - QTextEdit::keyPressEvent(event); - - if (isModifier) - return; - - if (emoji_popup_open_ && textAfterPosition(trigger_pos_).length() > 2) { - // Update completion - // Don't include the trigger token in the search - emoji_completion_model_->setFilterWildcard( - textAfterPosition(trigger_pos_).remove(0, 1)); - completer_->complete(completerRect()); - } - - if (emoji_popup_open_ && (completer_->completionCount() < 1 || - !textAfterPosition(trigger_pos_) - .contains(QRegularExpression(":[^\r\n\t\f\v :]+$")))) { - // No completions for this word or another word than the completer was - // started with - emoji_popup_open_ = false; - completer_->popup()->hide(); - } - - if (textCursor().position() == 0) { - resetAnchor(); - closeSuggestions(); - return; - } - - // Check if the current word should be autocompleted. - auto cursor = textCursor(); - cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); - auto word = cursor.selectedText(); - - if (hasAnchor(cursor.position(), anchorType_) && isAnchorValid()) { - if (word.isEmpty()) { - closeSuggestions(); - return; - } - - emit showSuggestions(word); - } else { - resetAnchor(); - closeSuggestions(); - } - - break; - } -} - -QRect -FilteredTextEdit::completerRect() -{ - // Move left edge to the beginning of the word - auto cursor = textCursor(); - auto rect = cursorRect(); - cursor.movePosition( - QTextCursor::Left, QTextCursor::MoveAnchor, textAfterPosition(trigger_pos_).length()); - auto cursor_global_x = viewport()->mapToGlobal(cursorRect(cursor).topLeft()).x(); - auto rect_global_left = viewport()->mapToGlobal(rect.bottomLeft()).x(); - auto dx = qAbs(rect_global_left - cursor_global_x); - rect.moveLeft(rect.left() - dx); - - auto item_height = completer_->popup()->sizeHintForRow(0); - auto max_height = item_height * completer_->maxVisibleItems(); - auto height = (completer_->completionCount() > completer_->maxVisibleItems()) - ? max_height - : completer_->completionCount() * item_height; - rect.setWidth(completer_->popup()->sizeHintForColumn(0)); - rect.moveBottom(-height); - return rect; -} - -QSize -FilteredTextEdit::sizeHint() const -{ - ensurePolished(); - auto margins = viewportMargins(); - margins += document()->documentMargin(); - QSize size = document()->size().toSize(); - size.rwidth() += margins.left() + margins.right(); - size.rheight() += margins.top() + margins.bottom(); - return size; -} - -QSize -FilteredTextEdit::minimumSizeHint() const -{ - ensurePolished(); - auto margins = viewportMargins(); - margins += document()->documentMargin(); - margins += contentsMargins(); - QSize size(fontMetrics().averageCharWidth() * 10, - fontMetrics().lineSpacing() + margins.top() + margins.bottom()); - return size; -} - -void -FilteredTextEdit::submit() -{} - -void -FilteredTextEdit::textChanged() -{ - working_history_[history_index_] = toPlainText(); -} - -TextInputWidget::TextInputWidget(QWidget *parent) - : QWidget(parent) -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - const int fontHeight = QFontMetrics(f).height(); - const int contentHeight = static_cast<int>(fontHeight * 2.5); - const int InputHeight = static_cast<int>(fontHeight * 1.5); - - setFixedHeight(contentHeight); - setCursor(Qt::ArrowCursor); - - topLayout_ = new QHBoxLayout(); - topLayout_->setSpacing(0); - topLayout_->setContentsMargins(13, 1, 13, 0); - - QIcon send_file_icon; - send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); - - sendFileBtn_ = new FlatButton(this); - sendFileBtn_->setToolTip(tr("Send a file")); - sendFileBtn_->setIcon(send_file_icon); - sendFileBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); - - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(InputHeight); - spinner_->setFixedWidth(InputHeight); - spinner_->setObjectName("FileUploadSpinner"); - spinner_->hide(); - - input_ = new FilteredTextEdit(this); - input_->setFixedHeight(InputHeight); - input_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - input_->setPlaceholderText(tr("Write a message...")); - - connect(input_, - &FilteredTextEdit::heightChanged, - this, - [this, InputHeight, contentHeight](int height) { - int widgetHeight = - std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, contentHeight)); - int textInputHeight = - std::min(widgetHeight - 1, std::max(height, InputHeight)); - - setFixedHeight(widgetHeight); - input_->setFixedHeight(textInputHeight); - - emit heightChanged(widgetHeight); - }); - connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty()) - return; - - QtConcurrent::run([this, q = q.toLower().toStdString()]() { - try { - emit input_->resultsRetrieved(cache::searchUsers( - ChatPage::instance()->currentRoom().toStdString(), q)); - } catch (const lmdb::error &e) { - nhlog::db()->error("Suggestion retrieval failed: {}", e.what()); - } - }); - }); - - sendMessageBtn_ = new FlatButton(this); - sendMessageBtn_->setToolTip(tr("Send a message")); - - QIcon send_message_icon; - send_message_icon.addFile(":/icons/icons/ui/cursor.png"); - sendMessageBtn_->setIcon(send_message_icon); - sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); - - topLayout_->addWidget(sendFileBtn_); - topLayout_->addWidget(input_); - topLayout_->addWidget(sendMessageBtn_); - - setLayout(topLayout_); - - connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); - connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); -} - -void -TextInputWidget::focusInEvent(QFocusEvent *event) -{ - input_->setFocus(event->reason()); -} - -void -TextInputWidget::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -}