From 4ccb5ed81f1787a3cca12e5ca705fad4e3fa8ca5 Mon Sep 17 00:00:00 2001 From: Benjamin Saunders Date: Sun, 5 Nov 2017 13:01:21 -0800 Subject: [PATCH] Add input history, enable multi-line input, refactor commands (#119) This also fixes the transmission of mis-typed commands as messages, fixes inability to send messages that start with a command, and does some initial work towards automatically resizing the input field to fit the input message. --- include/TextInputWidget.h | 32 +++++--- src/TextInputWidget.cc | 164 ++++++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 61 deletions(-) diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h index 32da6ba3..70b1c213 100644 --- a/include/TextInputWidget.h +++ b/include/TextInputWidget.h @@ -17,6 +17,8 @@ #pragma once +#include + #include #include #include @@ -29,26 +31,36 @@ namespace msgs = matrix::events::messages; -static const QString EMOTE_COMMAND("/me "); -static const QString JOIN_COMMAND("/join "); - class FilteredTextEdit : public QTextEdit { Q_OBJECT -private: - QTimer *typingTimer_; - public: explicit FilteredTextEdit(QWidget *parent = nullptr); - void keyPressEvent(QKeyEvent *event); void stopTyping(); + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void submit(); + signals: - void enterPressed(); void startedTyping(); void stoppedTyping(); + void message(QString); + void command(QString name, QString args); + +protected: + void keyPressEvent(QKeyEvent *event) override; + +private: + std::deque true_history_, working_history_; + size_t history_index_; + QTimer *typingTimer_; + + void textChanged(); + void afterCompletion(int); }; class TextInputWidget : public QFrame @@ -62,7 +74,6 @@ public: void stopTyping(); public slots: - void onSendButtonClicked(); void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); }; @@ -84,8 +95,7 @@ protected: private: void showUploadSpinner(); - QString parseEmoteCommand(const QString &cmd); - QString parseJoinCommand(const QString &cmd); + void command(QString name, QString args); QHBoxLayout *topLayout_; FilteredTextEdit *input_; diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc index cad54d96..e7c48b5f 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -25,9 +26,21 @@ #include "Config.h" #include "TextInputWidget.h" +static constexpr size_t INPUT_HISTORY_SIZE = 127; + FilteredTextEdit::FilteredTextEdit(QWidget *parent) - : QTextEdit(parent) + : QTextEdit{ parent } + , history_index_{ 0 } { + connect(document()->documentLayout(), + &QAbstractTextDocumentLayout::documentSizeChanged, + this, + &FilteredTextEdit::updateGeometry); + QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Fixed); + policy.setHeightForWidth(true); + setSizePolicy(policy); + working_history_.push_back(""); + connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); setAcceptRichText(false); typingTimer_ = new QTimer(this); @@ -49,12 +62,40 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) typingTimer_->start(); } - if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { - stopTyping(); - - emit enterPressed(); - } else { + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + if (!(event->modifiers() & Qt::ShiftModifier)) { + stopTyping(); + submit(); + } else { + QTextEdit::keyPressEvent(event); + } + break; + case Qt::Key_Up: { + auto initial_cursor = textCursor(); QTextEdit::keyPressEvent(event); + if (textCursor() == initial_cursor && + history_index_ + 1 < working_history_.size()) { + ++history_index_; + setPlainText(working_history_[history_index_]); + moveCursor(QTextCursor::End); + } + break; + } + case Qt::Key_Down: { + auto initial_cursor = textCursor(); + QTextEdit::keyPressEvent(event); + if (textCursor() == initial_cursor && history_index_ > 0) { + --history_index_; + setPlainText(working_history_[history_index_]); + moveCursor(QTextCursor::End); + } + break; + } + default: + QTextEdit::keyPressEvent(event); + break; } } @@ -65,6 +106,65 @@ FilteredTextEdit::stopTyping() emit stoppedTyping(); } +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() +{ + if (true_history_.size() == INPUT_HISTORY_SIZE) + true_history_.pop_back(); + true_history_.push_front(toPlainText()); + working_history_ = true_history_; + working_history_.push_front(""); + history_index_ = 0; + + QString text = toPlainText(); + if (text.startsWith('/')) { + int command_end = text.indexOf(' '); + if (command_end == -1) + command_end = text.size(); + auto name = text.mid(1, command_end - 1); + auto args = text.mid(command_end + 1); + if (name.isEmpty() || name == "/") { + message(args); + } else { + command(name, args); + } + } else { + message(std::move(text)); + } + + clear(); +} + +void +FilteredTextEdit::textChanged() +{ + working_history_[history_index_] = toPlainText(); +} + TextInputWidget::TextInputWidget(QWidget *parent) : QFrame(parent) { @@ -122,9 +222,10 @@ TextInputWidget::TextInputWidget(QWidget *parent) setLayout(topLayout_); - connect(sendMessageBtn_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked())); + connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, SIGNAL(enterPressed()), sendMessageBtn_, SIGNAL(clicked())); + connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); + connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -160,50 +261,13 @@ TextInputWidget::addSelectedEmoji(const QString &emoji) } void -TextInputWidget::onSendButtonClicked() +TextInputWidget::command(QString command, QString args) { - auto msgText = input_->document()->toPlainText().trimmed(); - - if (msgText.isEmpty()) - return; - - if (msgText.startsWith(EMOTE_COMMAND)) { - auto text = parseEmoteCommand(msgText); - - if (!text.isEmpty()) - emit sendEmoteMessage(text); - } else if (msgText.startsWith(JOIN_COMMAND)) { - auto room = parseJoinCommand(msgText); - - if (!room.isEmpty()) - emit sendJoinRoomRequest(room); - } else { - emit sendTextMessage(msgText); + if (command == "me") { + sendEmoteMessage(args); + } else if (command == "join") { + sendJoinRoomRequest(args); } - - input_->clear(); -} - -QString -TextInputWidget::parseJoinCommand(const QString &cmd) -{ - auto room = cmd.right(cmd.size() - JOIN_COMMAND.size()).trimmed(); - - if (!room.isEmpty()) - return room; - - return QString(""); -} - -QString -TextInputWidget::parseEmoteCommand(const QString &cmd) -{ - auto text = cmd.right(cmd.size() - EMOTE_COMMAND.size()).trimmed(); - - if (!text.isEmpty()) - return text; - - return QString(""); } void