Newer
Older
/*
* 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 <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QFileDialog>
#include <QImageReader>
#include <QMimeDatabase>
#include <QMimeType>
static constexpr size_t INPUT_HISTORY_SIZE = 127;
FilteredTextEdit::FilteredTextEdit(QWidget *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);
typingTimer_ = new QTimer(this);
typingTimer_->setInterval(1000);
typingTimer_->setSingleShot(true);
connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping);
connect(&previewDialog_,
&dialogs::PreviewImageOverlay::confirmImageUpload,
this,
&FilteredTextEdit::receiveImage);
previewDialog_.hide();
void
FilteredTextEdit::keyPressEvent(QKeyEvent *event)
const bool isModifier = (event->modifiers() != Qt::NoModifier);
if (!isModifier) {
if (!typingTimer_->isActive())
emit startedTyping();
typingTimer_->start();
}
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);
Konstantinos Sideris
committed
if (textCursor() == initial_cursor && textCursor().atStart() &&
history_index_ + 1 < working_history_.size()) {
++history_index_;
setPlainText(working_history_[history_index_]);
moveCursor(QTextCursor::End);
}
Konstantinos Sideris
committed
// Move to the start of the text if there aren't any lines to move up to.
if (textCursor() == initial_cursor) {
initial_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1);
setTextCursor(initial_cursor);
}
break;
}
case Qt::Key_Down: {
auto initial_cursor = textCursor();
QTextEdit::keyPressEvent(event);
Konstantinos Sideris
committed
if (textCursor() == initial_cursor && textCursor().atEnd() && history_index_ > 0) {
--history_index_;
setPlainText(working_history_[history_index_]);
moveCursor(QTextCursor::End);
}
Konstantinos Sideris
committed
// Move to the end of the text if there aren't any lines to move down to.
if (textCursor() == initial_cursor) {
initial_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor, 1);
setTextCursor(initial_cursor);
}
break;
}
default:
QTextEdit::keyPressEvent(event);
break;
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
bool
FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const
{
return (source->hasImage() || QTextEdit::canInsertFromMimeData(source));
}
void
FilteredTextEdit::insertFromMimeData(const QMimeData *source)
{
if (source->hasImage()) {
const auto formats = source->formats();
const auto idx = formats.indexOf(
QRegularExpression{"image/.+", QRegularExpression::CaseInsensitiveOption});
// Note: in the future we may want to look into what the best choice is from the
// formats list. For now we will default to PNG format.
QString type = "png";
if (idx != -1) {
type = formats.at(idx).split('/')[1];
}
// Encode raw pixel data of image.
QByteArray data = source->data("image/" + type);
previewDialog_.setImageAndCreate(data, type);
previewDialog_.show();
} else if (source->hasFormat("x-special/gnome-copied-files") &&
QImageReader{source->text()}.canRead()) {
// Special case for X11 users. See "Notes for X11 Users" in source.
// Source: http://doc.qt.io/qt-5/qclipboard.html
previewDialog_.setImageAndCreate(source->text());
previewDialog_.show();
} else {
QTextEdit::insertFromMimeData(source);
}
}
void
FilteredTextEdit::stopTyping()
{
typingTimer_->stop();
emit stoppedTyping();
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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 (toPlainText().trimmed().isEmpty())
return;
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();
}
void
FilteredTextEdit::receiveImage(const QByteArray img, const QString &img_name)
{
QSharedPointer<QBuffer> buffer{new QBuffer{this}};
buffer->setData(img);
emit image(buffer, img_name);
}
setFont(QFont("Emoji One"));
setFixedHeight(conf::textInput::height);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setCursor(Qt::ArrowCursor);
topLayout_->setContentsMargins(15, 0, 15, 5);
send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png");
sendFileBtn_ = new FlatButton(this);
sendFileBtn_->setIcon(send_file_icon);
sendFileBtn_->setIconSize(QSize(24, 24));
spinner_ = new LoadingIndicator(this);
spinner_->setFixedHeight(32);
spinner_->setFixedWidth(32);
spinner_->setObjectName("FileUploadSpinner");
font.setPixelSize(conf::textInputFontSize);
input_ = new FilteredTextEdit(this);
input_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
input_->setPlaceholderText(tr("Write a message..."));
input_->setStyleSheet("border: none; padding-top: 5px;");
sendMessageBtn_ = new FlatButton(this);
QIcon send_message_icon;
send_message_icon.addFile(":/icons/icons/ui/cursor.png");
sendMessageBtn_->setIcon(send_message_icon);
sendMessageBtn_->setIconSize(QSize(24, 24));
emojiBtn_ = new emoji::PickButton(this);
emoji_icon.addFile(":/icons/icons/ui/smile.png");
emojiBtn_->setIcon(emoji_icon);
emojiBtn_->setIconSize(QSize(24, 24));
topLayout_->addWidget(sendFileBtn_);
topLayout_->addWidget(input_);
topLayout_->addWidget(emojiBtn_);
topLayout_->addWidget(sendMessageBtn_);
connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit);
connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection()));
connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage);
connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command);
connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage);
SIGNAL(emojiSelected(const QString &)),
this,
SLOT(addSelectedEmoji(const QString &)));
connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping);
connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping);
void
TextInputWidget::addSelectedEmoji(const QString &emoji)
QTextCursor cursor = input_->textCursor();
QFont emoji_font("Emoji One");
emoji_font.setPixelSize(conf::emojiSize);
QFont text_font("Open Sans");
text_font.setPixelSize(conf::fontSize);
QTextCharFormat charfmt;
charfmt.setFont(emoji_font);
input_->setCurrentCharFormat(charfmt);
input_->insertPlainText(emoji);
cursor.movePosition(QTextCursor::End);
charfmt.setFont(text_font);
input_->setCurrentCharFormat(charfmt);
TextInputWidget::command(QString command, QString args)
if (command == "me") {
sendEmoteMessage(args);
} else if (command == "join") {
sendJoinRoomRequest(args);
void
TextInputWidget::openFileSelection()
{
const auto fileName =
QFileDialog::getOpenFileName(this, tr("Select a file"), "", tr("All Files (*)"));
if (fileName.isEmpty())
return;
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
const auto format = mime.name().split("/")[0];
QSharedPointer<QFile> file{new QFile{fileName, this}};
emit uploadImage(file, fileName);
emit uploadAudio(file, fileName);
emit uploadFile(file, fileName);
showUploadSpinner();
}
void
TextInputWidget::showUploadSpinner()
{
topLayout_->removeWidget(sendFileBtn_);
sendFileBtn_->hide();
topLayout_->insertWidget(0, spinner_);
spinner_->start();
}
void
TextInputWidget::hideUploadSpinner()
{
topLayout_->removeWidget(spinner_);
topLayout_->insertWidget(0, sendFileBtn_);
sendFileBtn_->show();
spinner_->stop();
}
TextInputWidget::~TextInputWidget() {}
void
TextInputWidget::stopTyping()
{
input_->stopTyping();
}
void
TextInputWidget::focusInEvent(QFocusEvent *event)
{
input_->setFocus(event->reason());
}