diff options
author | christarazi <christarazi@users.noreply.github.com> | 2018-01-09 23:52:59 -0800 |
---|---|---|
committer | mujx <mujx@users.noreply.github.com> | 2018-01-10 09:52:59 +0200 |
commit | ddfce136ed4de5b80ae5961b6a11363624e0b244 (patch) | |
tree | b3d54b98832f1043d36fe652099625e0579c058e /src | |
parent | Make group's sidebar visible through an option (diff) | |
download | nheko-ddfce136ed4de5b80ae5961b6a11363624e0b244.tar.xz |
Add support for pasting images into a room (#180)
fixes #132
Diffstat (limited to 'src')
-rw-r--r-- | src/ChatPage.cc | 31 | ||||
-rw-r--r-- | src/MatrixClient.cc | 44 | ||||
-rw-r--r-- | src/TextInputWidget.cc | 65 | ||||
-rw-r--r-- | src/dialogs/PreviewImageOverlay.cc | 142 | ||||
-rw-r--r-- | src/timeline/TimelineView.cc | 8 | ||||
-rw-r--r-- | src/timeline/TimelineViewManager.cc | 7 | ||||
-rw-r--r-- | src/timeline/widgets/AudioItem.cc | 6 | ||||
-rw-r--r-- | src/timeline/widgets/FileItem.cc | 6 | ||||
-rw-r--r-- | src/timeline/widgets/ImageItem.cc | 17 | ||||
-rw-r--r-- | src/timeline/widgets/VideoItem.cc | 2 |
10 files changed, 278 insertions, 50 deletions
diff --git a/src/ChatPage.cc b/src/ChatPage.cc index ebdec835..f49c0a08 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -228,17 +228,26 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, client_.data(), &MatrixClient::joinRoom); - connect(text_input_, &TextInputWidget::uploadImage, this, [=](QString filename) { - client_->uploadImage(current_room_, filename); - }); + connect(text_input_, + &TextInputWidget::uploadImage, + this, + [=](QSharedPointer<QIODevice> data, const QString &fn) { + client_->uploadImage(current_room_, data, fn); + }); - connect(text_input_, &TextInputWidget::uploadFile, this, [=](QString filename) { - client_->uploadFile(current_room_, filename); - }); + connect(text_input_, + &TextInputWidget::uploadFile, + this, + [=](QSharedPointer<QIODevice> data, const QString &fn) { + client_->uploadFile(current_room_, data, fn); + }); - connect(text_input_, &TextInputWidget::uploadAudio, this, [=](QString filename) { - client_->uploadAudio(current_room_, filename); - }); + connect(text_input_, + &TextInputWidget::uploadAudio, + this, + [=](QSharedPointer<QIODevice> data, const QString &fn) { + client_->uploadAudio(current_room_, data, fn); + }); connect( client_.data(), &MatrixClient::roomCreationFailed, this, &ChatPage::showNotification); @@ -246,9 +255,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, connect(client_.data(), &MatrixClient::imageUploaded, this, - [=](QString roomid, QString filename, QString url) { + [=](QString roomid, QSharedPointer<QIODevice> data, QString filename, QString url) { text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage(roomid, filename, url); + view_manager_->queueImageMessage(roomid, data, filename, url); }); connect(client_.data(), &MatrixClient::fileUploaded, diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index 72467385..67203d59 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -821,14 +821,16 @@ MatrixClient::messages(const QString &roomid, const QString &from_token, int lim } void -MatrixClient::uploadImage(const QString &roomid, const QString &filename) +MatrixClient::uploadImage(const QString &roomid, + const QSharedPointer<QIODevice> data, + const QString &filename) { - auto reply = makeUploadRequest(filename); + auto reply = makeUploadRequest(data); if (reply == nullptr) return; - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -838,12 +840,12 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename) return; } - auto data = reply->readAll(); + auto res_data = reply->readAll(); - if (data.isEmpty()) + if (res_data.isEmpty()) return; - auto json = QJsonDocument::fromJson(data); + auto json = QJsonDocument::fromJson(res_data); if (!json.isObject()) { qDebug() << "Media upload: Response is not a json object."; @@ -857,16 +859,18 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename) return; } - emit imageUploaded(roomid, filename, object.value("content_uri").toString()); + emit imageUploaded(roomid, data, filename, object.value("content_uri").toString()); }); } void -MatrixClient::uploadFile(const QString &roomid, const QString &filename) +MatrixClient::uploadFile(const QString &roomid, + const QSharedPointer<QIODevice> data, + const QString &filename) { - auto reply = makeUploadRequest(filename); + auto reply = makeUploadRequest(data); - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -900,11 +904,13 @@ MatrixClient::uploadFile(const QString &roomid, const QString &filename) } void -MatrixClient::uploadAudio(const QString &roomid, const QString &filename) +MatrixClient::uploadAudio(const QString &roomid, + const QSharedPointer<QIODevice> data, + const QString &filename) { - auto reply = makeUploadRequest(filename); + auto reply = makeUploadRequest(data); - connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, filename]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, roomid, data, filename]() { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -1158,7 +1164,7 @@ MatrixClient::readEvent(const QString &room_id, const QString &event_id) } QNetworkReply * -MatrixClient::makeUploadRequest(const QString &filename) +MatrixClient::makeUploadRequest(QSharedPointer<QIODevice> iodev) { QUrlQuery query; query.addQueryItem("access_token", token_); @@ -1167,20 +1173,18 @@ MatrixClient::makeUploadRequest(const QString &filename) endpoint.setPath(mediaApiUrl_ + "/upload"); endpoint.setQuery(query); - QFile file(filename); - if (!file.open(QIODevice::ReadWrite)) { - qDebug() << "Error while reading" << filename; + if (!iodev->open(QIODevice::ReadOnly)) { + qWarning() << "Error while reading device:" << iodev->errorString(); return nullptr; } QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(filename, QMimeDatabase::MatchContent); + QMimeType mime = db.mimeTypeForData(iodev.data()); QNetworkRequest request(QString(endpoint.toEncoded())); - request.setHeader(QNetworkRequest::ContentLengthHeader, file.size()); request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name()); - auto reply = post(request, file.readAll()); + auto reply = post(request, iodev.data()); return reply; } diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc index dc2bebe7..f9198c78 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc @@ -16,10 +16,13 @@ */ #include <QAbstractTextDocumentLayout> +#include <QApplication> +#include <QBuffer> +#include <QClipboard> #include <QDebug> -#include <QFile> #include <QFileDialog> #include <QImageReader> +#include <QMimeData> #include <QMimeDatabase> #include <QMimeType> #include <QPainter> @@ -33,6 +36,7 @@ static constexpr size_t INPUT_HISTORY_SIZE = 127; FilteredTextEdit::FilteredTextEdit(QWidget *parent) : QTextEdit{parent} , history_index_{0} + , previewDialog_{parent} { connect(document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, @@ -50,6 +54,12 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) typingTimer_->setSingleShot(true); connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); + connect(&previewDialog_, + &dialogs::PreviewImageOverlay::confirmImageUpload, + this, + &FilteredTextEdit::receiveImage); + + previewDialog_.hide(); } void @@ -101,6 +111,42 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } +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() { @@ -146,6 +192,7 @@ FilteredTextEdit::submit() history_index_ = 0; QString text = toPlainText(); + if (text.startsWith('/')) { int command_end = text.indexOf(' '); if (command_end == -1) @@ -170,6 +217,14 @@ 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); +} + TextInputWidget::TextInputWidget(QWidget *parent) : QFrame(parent) { @@ -231,6 +286,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) 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); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -289,12 +345,13 @@ TextInputWidget::openFileSelection() const auto format = mime.name().split("/")[0]; + QSharedPointer<QFile> file{new QFile{fileName, this}}; if (format == "image") - emit uploadImage(fileName); + emit uploadImage(file, fileName); else if (format == "audio") - emit uploadAudio(fileName); + emit uploadAudio(file, fileName); else - emit uploadFile(fileName); + emit uploadFile(file, fileName); showUploadSpinner(); } diff --git a/src/dialogs/PreviewImageOverlay.cc b/src/dialogs/PreviewImageOverlay.cc new file mode 100644 index 00000000..31ef00ed --- /dev/null +++ b/src/dialogs/PreviewImageOverlay.cc @@ -0,0 +1,142 @@ +/* + * 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 <QApplication> +#include <QBuffer> +#include <QDebug> +#include <QFile> +#include <QFileInfo> +#include <QHBoxLayout> +#include <QVBoxLayout> + +#include "Config.h" + +#include "dialogs/PreviewImageOverlay.h" + +using namespace dialogs; + +static constexpr const char *DEFAULT = "Upload image?"; +static constexpr const char *ERROR = "Failed to load image type '%1'. Continue upload?"; + +PreviewImageOverlay::PreviewImageOverlay(QWidget *parent) + : QWidget{parent} + , titleLabel_{tr(DEFAULT), this} + , imageLabel_{this} + , imageName_{tr("clipboard"), this} + , upload_{tr("Upload"), this} + , cancel_{tr("Cancel"), this} +{ + auto hlayout = new QHBoxLayout; + hlayout->addWidget(&upload_); + hlayout->addWidget(&cancel_); + + auto vlayout = new QVBoxLayout{this}; + vlayout->addWidget(&titleLabel_); + vlayout->addWidget(&imageLabel_); + vlayout->addWidget(&imageName_); + vlayout->addLayout(hlayout); + + connect(&upload_, &QPushButton::clicked, [&]() { + emit confirmImageUpload(imageData_, imageName_.text()); + close(); + }); + connect(&cancel_, &QPushButton::clicked, [&]() { close(); }); +} + +void +PreviewImageOverlay::init() +{ + auto window = QApplication::activeWindow(); + auto winsize = window->frameGeometry().size(); + auto center = window->frameGeometry().center(); + auto img_size = image_.size(); + + imageName_.setText(QFileInfo{imagePath_}.fileName()); + + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + titleLabel_.setStyleSheet( + QString{"font-weight: bold; font-size: %1px;"}.arg(conf::headerFontSize)); + titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + titleLabel_.setAlignment(Qt::AlignCenter); + imageLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + imageLabel_.setAlignment(Qt::AlignCenter); + imageName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + imageName_.setAlignment(Qt::AlignCenter); + upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + upload_.setFontSize(conf::btn::fontSize); + cancel_.setFontSize(conf::btn::fontSize); + + // Scale image preview to the size of the current window if it is larger. + if ((img_size.height() * img_size.width()) > (winsize.height() * winsize.width())) { + imageLabel_.setPixmap(image_.scaled(winsize, Qt::KeepAspectRatio)); + } else { + imageLabel_.setPixmap(image_); + move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); + } + imageLabel_.setScaledContents(false); + + raise(); +} + +void +PreviewImageOverlay::setImageAndCreate(const QByteArray data, const QString &type) +{ + imageData_ = data; + imagePath_ = "clipboard." + type; + auto loaded = image_.loadFromData(imageData_); + if (!loaded) { + titleLabel_.setText(QString{tr(ERROR)}.arg(type)); + } else { + titleLabel_.setText(tr(DEFAULT)); + } + + init(); +} + +void +PreviewImageOverlay::setImageAndCreate(const QString &path) +{ + QFile file{path}; + imagePath_ = path; + + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open image from:" << path; + qWarning() << "Reason:" << file.errorString(); + close(); + return; + } + + if ((imageData_ = file.readAll()).isEmpty()) { + qWarning() << "Failed to read image:" << file.errorString(); + close(); + return; + } + + auto loaded = image_.loadFromData(imageData_); + if (!loaded) { + auto t = QFileInfo{path}.suffix(); + titleLabel_.setText(QString{tr(ERROR)}.arg(t)); + } else { + titleLabel_.setText(tr(DEFAULT)); + } + + init(); +} diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index d21f30f0..75ce8141 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -510,12 +510,8 @@ TimelineView::sendNextPendingMessage() case mtx::events::MessageType::Image: case mtx::events::MessageType::File: // FIXME: Improve the API - client_->sendRoomMessage(m.ty, - m.txn_id, - room_id_, - QFileInfo(m.filename).fileName(), - QFileInfo(m.filename), - m.body); + client_->sendRoomMessage( + m.ty, m.txn_id, room_id_, m.filename, QFileInfo(m.filename), m.body); break; default: client_->sendRoomMessage(m.ty, m.txn_id, room_id_, m.body, QFileInfo()); diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc index de1e1e32..65c9ac83 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cc @@ -85,6 +85,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg) void TimelineViewManager::queueImageMessage(const QString &roomid, + const QSharedPointer<QIODevice> data, const QString &filename, const QString &url) { @@ -95,7 +96,7 @@ TimelineViewManager::queueImageMessage(const QString &roomid, auto view = views_[roomid]; - view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(url, filename); + view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(url, data, filename); } void @@ -110,7 +111,7 @@ TimelineViewManager::queueFileMessage(const QString &roomid, auto view = views_[roomid]; - view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename); + view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, nullptr, filename); } void @@ -125,7 +126,7 @@ TimelineViewManager::queueAudioMessage(const QString &roomid, auto view = views_[roomid]; - view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename); + view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, nullptr, filename); } void diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cc index 5d9dd77b..e84cbb3a 100644 --- a/src/timeline/widgets/AudioItem.cc +++ b/src/timeline/widgets/AudioItem.cc @@ -89,14 +89,16 @@ AudioItem::AudioItem(QSharedPointer<MatrixClient> client, AudioItem::AudioItem(QSharedPointer<MatrixClient> client, const QString &url, + const QSharedPointer<QIODevice> data, const QString &filename, QWidget *parent) : QWidget(parent) , url_{url} - , text_{QFileInfo(filename).fileName()} + , text_{QFileInfo{filename}.fileName()} , client_{client} { - readableFileSize_ = calculateFileSize(QFileInfo(filename).size()); + Q_UNUSED(data); + readableFileSize_ = calculateFileSize(QFileInfo{filename}.size()); init(); } diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cc index 3c38dc31..a6159309 100644 --- a/src/timeline/widgets/FileItem.cc +++ b/src/timeline/widgets/FileItem.cc @@ -76,14 +76,16 @@ FileItem::FileItem(QSharedPointer<MatrixClient> client, FileItem::FileItem(QSharedPointer<MatrixClient> client, const QString &url, + const QSharedPointer<QIODevice> data, const QString &filename, QWidget *parent) : QWidget(parent) , url_{url} - , text_{QFileInfo(filename).fileName()} + , text_{QFileInfo{filename}.fileName()} , client_{client} { - readableFileSize_ = calculateFileSize(QFileInfo(filename).size()); + Q_UNUSED(data); + readableFileSize_ = calculateFileSize(QFileInfo{filename}.size()); init(); } diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cc index 9038456d..48a4c1eb 100644 --- a/src/timeline/widgets/ImageItem.cc +++ b/src/timeline/widgets/ImageItem.cc @@ -61,11 +61,12 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client, ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const QString &url, + const QSharedPointer<QIODevice> data, const QString &filename, QWidget *parent) : QWidget(parent) , url_{url} - , text_{QFileInfo(filename).fileName()} + , text_{filename} , client_{client} { setMouseTracking(true); @@ -83,7 +84,19 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client, url_ = QString("%1/_matrix/media/r0/download/%2") .arg(client_.data()->getHomeServer().toString(), media_params); - setImage(QPixmap(filename)); + if (data.isNull()) { + qWarning() << "No image data to display"; + return; + } + + if (data->reset()) { + QPixmap p; + p.loadFromData(data->readAll()); + setImage(p); + } else { + qWarning() << "Failed to seek to beginning of device:" << data->errorString(); + return; + } } void diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cc index b3987b83..b46dff7b 100644 --- a/src/timeline/widgets/VideoItem.cc +++ b/src/timeline/widgets/VideoItem.cc @@ -66,6 +66,7 @@ VideoItem::VideoItem(QSharedPointer<MatrixClient> client, VideoItem::VideoItem(QSharedPointer<MatrixClient> client, const QString &url, + const QSharedPointer<QIODevice> data, const QString &filename, QWidget *parent) : QWidget(parent) @@ -73,6 +74,7 @@ VideoItem::VideoItem(QSharedPointer<MatrixClient> client, , text_{QFileInfo(filename).fileName()} , client_{client} { + Q_UNUSED(data); readableFileSize_ = calculateFileSize(QFileInfo(filename).size()); init(); |