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.
This commit is contained in:
Benjamin Saunders 2017-11-05 13:01:21 -08:00 committed by mujx
parent 2929d4a3a4
commit 4ccb5ed81f
2 changed files with 135 additions and 61 deletions

View file

@ -17,6 +17,8 @@
#pragma once #pragma once
#include <deque>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPaintEvent> #include <QPaintEvent>
#include <QTextEdit> #include <QTextEdit>
@ -29,26 +31,36 @@
namespace msgs = matrix::events::messages; namespace msgs = matrix::events::messages;
static const QString EMOTE_COMMAND("/me ");
static const QString JOIN_COMMAND("/join ");
class FilteredTextEdit : public QTextEdit class FilteredTextEdit : public QTextEdit
{ {
Q_OBJECT Q_OBJECT
private:
QTimer *typingTimer_;
public: public:
explicit FilteredTextEdit(QWidget *parent = nullptr); explicit FilteredTextEdit(QWidget *parent = nullptr);
void keyPressEvent(QKeyEvent *event);
void stopTyping(); void stopTyping();
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
void submit();
signals: signals:
void enterPressed();
void startedTyping(); void startedTyping();
void stoppedTyping(); void stoppedTyping();
void message(QString);
void command(QString name, QString args);
protected:
void keyPressEvent(QKeyEvent *event) override;
private:
std::deque<QString> true_history_, working_history_;
size_t history_index_;
QTimer *typingTimer_;
void textChanged();
void afterCompletion(int);
}; };
class TextInputWidget : public QFrame class TextInputWidget : public QFrame
@ -62,7 +74,6 @@ public:
void stopTyping(); void stopTyping();
public slots: public slots:
void onSendButtonClicked();
void openFileSelection(); void openFileSelection();
void hideUploadSpinner(); void hideUploadSpinner();
void focusLineEdit() { input_->setFocus(); }; void focusLineEdit() { input_->setFocus(); };
@ -84,8 +95,7 @@ protected:
private: private:
void showUploadSpinner(); void showUploadSpinner();
QString parseEmoteCommand(const QString &cmd); void command(QString name, QString args);
QString parseJoinCommand(const QString &cmd);
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
FilteredTextEdit *input_; FilteredTextEdit *input_;

View file

@ -15,6 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <QAbstractTextDocumentLayout>
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include <QFileDialog> #include <QFileDialog>
@ -25,9 +26,21 @@
#include "Config.h" #include "Config.h"
#include "TextInputWidget.h" #include "TextInputWidget.h"
static constexpr size_t INPUT_HISTORY_SIZE = 127;
FilteredTextEdit::FilteredTextEdit(QWidget *parent) 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); setAcceptRichText(false);
typingTimer_ = new QTimer(this); typingTimer_ = new QTimer(this);
@ -49,12 +62,40 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
typingTimer_->start(); typingTimer_->start();
} }
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { switch (event->key()) {
stopTyping(); case Qt::Key_Return:
case Qt::Key_Enter:
emit enterPressed(); if (!(event->modifiers() & Qt::ShiftModifier)) {
} else { stopTyping();
submit();
} else {
QTextEdit::keyPressEvent(event);
}
break;
case Qt::Key_Up: {
auto initial_cursor = textCursor();
QTextEdit::keyPressEvent(event); 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(); 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) TextInputWidget::TextInputWidget(QWidget *parent)
: QFrame(parent) : QFrame(parent)
{ {
@ -122,9 +222,10 @@ TextInputWidget::TextInputWidget(QWidget *parent)
setLayout(topLayout_); setLayout(topLayout_);
connect(sendMessageBtn_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked())); connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); 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_, connect(emojiBtn_,
SIGNAL(emojiSelected(const QString &)), SIGNAL(emojiSelected(const QString &)),
this, this,
@ -160,50 +261,13 @@ TextInputWidget::addSelectedEmoji(const QString &emoji)
} }
void void
TextInputWidget::onSendButtonClicked() TextInputWidget::command(QString command, QString args)
{ {
auto msgText = input_->document()->toPlainText().trimmed(); if (command == "me") {
sendEmoteMessage(args);
if (msgText.isEmpty()) } else if (command == "join") {
return; sendJoinRoomRequest(args);
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);
} }
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 void