diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e68db77..52527312 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,7 +299,6 @@ set(SRC_FILES src/SSOHandler.cpp src/SideBarActions.cpp src/Splitter.cpp - src/TextInputWidget.cpp src/TrayIcon.cpp src/UserInfoWidget.cpp src/UserSettingsPage.cpp @@ -505,7 +504,6 @@ qt5_wrap_cpp(MOC_HEADERS src/SSOHandler.h src/SideBarActions.h src/Splitter.h - src/TextInputWidget.h src/TrayIcon.h src/UserInfoWidget.h src/UserSettingsPage.h diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 1b235a95..1fd9b9bc 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -39,7 +39,6 @@ #include "RoomList.h" #include "SideBarActions.h" #include "Splitter.h" -#include "TextInputWidget.h" #include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -138,18 +137,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) splitter->addWidget(content_); splitter->restoreSizes(parent->width()); - text_input_ = new TextInputWidget(this); - contentLayout_->addWidget(text_input_); - connect(this, &ChatPage::connectionLost, this, [this]() { nhlog::net()->info("connectivity lost"); isConnected_ = false; http::client()->shutdown(); - text_input_->disableInput(); }); connect(this, &ChatPage::connectionRestored, this, [this]() { nhlog::net()->info("trying to re-connect"); - text_input_->enableInput(); isConnected_ = true; // Drop all pending connections. @@ -573,7 +567,6 @@ ChatPage::showQuickSwitcher() connect(dialog, &QuickSwitcher::roomSelected, room_list_, &RoomList::highlightSelectedRoom); connect(dialog, &QuickSwitcher::closing, this, [this]() { MainWindow::instance()->hideOverlay(); - text_input_->setFocus(Qt::FocusReason::PopupFocusReason); }); MainWindow::instance()->showTransparentOverlayModal(dialog); diff --git a/src/ChatPage.h b/src/ChatPage.h index 37abafa0..273ba4af 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -46,7 +46,6 @@ class QuickSwitcher; class RoomList; class SideBarActions; class Splitter; -class TextInputWidget; class TimelineViewManager; class UserInfoWidget; class UserSettings; @@ -251,8 +250,6 @@ private: TimelineViewManager *view_manager_; SideBarActions *sidebarActions_; - TextInputWidget *text_input_; - QTimer connectivityTimer_; std::atomic_bool isConnected_; 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 - * - * 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 . - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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::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(); - }); - - 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 &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(fontHeight * 2.5); - const int InputHeight = static_cast(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); -} diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h deleted file mode 100644 index 19462f70..00000000 --- a/src/TextInputWidget.h +++ /dev/null @@ -1,173 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * 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 . - */ - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -#include "dialogs/PreviewUploadOverlay.h" -#include "popups/SuggestionsPopup.h" - -struct SearchResult; - -class CompletionProxyModel; -class FlatButton; -class LoadingIndicator; -class QCompleter; - -class FilteredTextEdit : public QTextEdit -{ - Q_OBJECT - -public: - explicit FilteredTextEdit(QWidget *parent = nullptr); - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - - void submit(); - -signals: - void heightChanged(int height); - void startedUpload(); - - //! Trigger the suggestion popup. - void showSuggestions(const QString &query); - void resultsRetrieved(const std::vector &results); - void selectNextSuggestion(); - void selectPreviousSuggestion(); - void selectHoveredSuggestion(); - -public slots: - void showResults(const std::vector &results); - -protected: - void keyPressEvent(QKeyEvent *event) override; - void focusOutEvent(QFocusEvent *event) override - { - suggestionsPopup_.hide(); - QTextEdit::focusOutEvent(event); - } - -private: - bool emoji_popup_open_ = false; - CompletionProxyModel *emoji_completion_model_; - std::deque true_history_, working_history_; - int trigger_pos_; // Where emoji completer was triggered - size_t history_index_; - QCompleter *completer_; - - SuggestionsPopup suggestionsPopup_; - - enum class AnchorType - { - Tab = 0, - Sigil = 1, - }; - - AnchorType anchorType_ = AnchorType::Sigil; - - int anchorWidth(AnchorType anchor) { return static_cast(anchor); } - - void closeSuggestions() { suggestionsPopup_.hide(); } - void resetAnchor() { atTriggerPosition_ = -1; } - bool isAnchorValid() { return atTriggerPosition_ != -1; } - bool hasAnchor(int pos, AnchorType anchor) - { - return pos == atTriggerPosition_ + anchorWidth(anchor); - } - QRect completerRect(); - QString query() - { - auto cursor = textCursor(); - cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); - return cursor.selectedText(); - } - QString textAfterPosition(int pos) - { - auto tc = textCursor(); - tc.setPosition(pos); - tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); - return tc.selectedText(); - } - - dialogs::PreviewUploadOverlay previewDialog_; - - //! Latest position of the '@' character that triggers the username completer. - int atTriggerPosition_ = -1; - - void insertCompletion(QString completion); - void textChanged(); - void afterCompletion(int); -}; - -class TextInputWidget : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) - -public: - TextInputWidget(QWidget *parent = nullptr); - - QColor borderColor() const { return borderColor_; } - void setBorderColor(QColor &color) { borderColor_ = color; } - void disableInput() - { - input_->setEnabled(false); - input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect...")); - } - void enableInput() - { - input_->setEnabled(true); - input_->setPlaceholderText(tr("Write a message...")); - } - -public slots: - void focusLineEdit() { input_->setFocus(); } - -signals: - void heightChanged(int height); - - void sendJoinRoomRequest(const QString &room); - void sendInviteRoomRequest(const QString &userid, const QString &reason); - void sendKickRoomRequest(const QString &userid, const QString &reason); - void sendBanRoomRequest(const QString &userid, const QString &reason); - void sendUnbanRoomRequest(const QString &userid, const QString &reason); - void changeRoomNick(const QString &displayname); - -protected: - void focusInEvent(QFocusEvent *event) override; - void paintEvent(QPaintEvent *) override; - -private: - QHBoxLayout *topLayout_; - FilteredTextEdit *input_; - - LoadingIndicator *spinner_; - - FlatButton *sendFileBtn_; - FlatButton *sendMessageBtn_; - QColor borderColor_; -};