From 0bb488563288da75566e7da883fda31914ecf281 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Nov 2020 03:12:37 +0100 Subject: Basic text input in qml --- src/ChatPage.cpp | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 78987fb9..6e1ed8ca 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -160,15 +160,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) trySync(); }); - connect(text_input_, - &TextInputWidget::clearRoomTimeline, - view_manager_, - &TimelineViewManager::clearCurrentRoomTimeline); - - connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() { - cache::dropOutboundMegolmSession(current_room_.toStdString()); - }); - connect( new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { if (isVisible()) @@ -277,45 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect(text_input_, - &TextInputWidget::sendTextMessage, - view_manager_, - &TimelineViewManager::queueTextMessage); - - connect(text_input_, - &TextInputWidget::sendEmoteMessage, - view_manager_, - &TimelineViewManager::queueEmoteMessage); - - connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); - - // invites and bans via quick command - connect(text_input_, &TextInputWidget::sendInviteRoomRequest, this, &ChatPage::inviteUser); - connect(text_input_, &TextInputWidget::sendKickRoomRequest, this, &ChatPage::kickUser); - connect(text_input_, &TextInputWidget::sendBanRoomRequest, this, &ChatPage::banUser); - connect(text_input_, &TextInputWidget::sendUnbanRoomRequest, this, &ChatPage::unbanUser); - - connect( - text_input_, &TextInputWidget::changeRoomNick, this, [this](const QString &displayName) { - mtx::events::state::Member member; - member.display_name = displayName.toStdString(); - member.avatar_url = - cache::avatarUrl(currentRoom(), - QString::fromStdString(http::client()->user_id().to_string())) - .toStdString(); - member.membership = mtx::events::state::Membership::Join; - - http::client()->send_state_event( - currentRoom().toStdString(), - http::client()->user_id().to_string(), - member, - [](mtx::responses::EventId, mtx::http::RequestErr err) { - if (err) - nhlog::net()->error("Failed to set room displayname: {}", - err->matrix_error.error); - }); - }); - connect( text_input_, &TextInputWidget::uploadMedia, -- cgit 1.5.1 From a31d3d08165646738d6ae624ac4eff6971207058 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 15 Nov 2020 04:52:49 +0100 Subject: Add file uploading --- resources/qml/ActiveCallBar.qml | 11 +- resources/qml/MessageInput.qml | 45 +++-- resources/qml/NhekoBusyIndicator.qml | 64 +++++++ resources/qml/TimelineView.qml | 5 +- resources/qml/VideoCall.qml | 3 +- resources/res.qrc | 1 + src/ChatPage.cpp | 120 ------------- src/ChatPage.h | 11 -- src/TextInputWidget.cpp | 146 ---------------- src/TextInputWidget.h | 14 -- src/Utils.cpp | 5 +- src/Utils.h | 2 +- src/dialogs/PreviewUploadOverlay.cpp | 5 +- src/dialogs/PreviewUploadOverlay.h | 1 + src/timeline/InputBar.cpp | 317 ++++++++++++++++++++++++++++++++++- src/timeline/InputBar.h | 41 +++++ src/timeline/TimelineModel.h | 2 +- 17 files changed, 475 insertions(+), 318 deletions(-) create mode 100644 resources/qml/NhekoBusyIndicator.qml (limited to 'src/ChatPage.cpp') diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 282cac81..cbe36e6d 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -12,8 +12,11 @@ Rectangle { MouseArea { anchors.fill: parent - onClicked: if (TimelineManager.onVideoCall) - stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1; + onClicked: { + if (TimelineManager.onVideoCall) + stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1; + + } } RowLayout { @@ -39,8 +42,7 @@ Rectangle { Image { Layout.preferredWidth: 24 Layout.preferredHeight: 24 - source: TimelineManager.onVideoCall ? - "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" + source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" } Label { @@ -69,6 +71,7 @@ Rectangle { callTimer.startTime = Math.floor(d.getTime() / 1000); if (TimelineManager.onVideoCall) stackLayout.currentIndex = 1; + break; case WebRTCState.DISCONNECTED: callStateLabel.text = ""; diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index b76a44f3..a1220599 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -2,7 +2,6 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 Rectangle { @@ -36,6 +35,20 @@ Rectangle { image: ":/icons/icons/ui/paper-clip-outline.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + onClicked: TimelineManager.timeline.input.openFileSelection() + + Rectangle { + anchors.fill: parent + color: colors.window + visible: TimelineManager.timeline.input.uploading + + NhekoBusyIndicator { + anchors.fill: parent + running: parent.visible + } + + } + } ScrollView { @@ -52,27 +65,27 @@ Rectangle { placeholderTextColor: colors.buttonText color: colors.text wrapMode: TextEdit.Wrap - onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onCursorPositionChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - - Connections { - target: TimelineManager.timeline.input - function onInsertText(text_) { textArea.insert(textArea.cursorPosition, text_); } - } - Keys.onPressed: { if (event.matches(StandardKey.Paste)) { - TimelineManager.timeline.input.paste(false) - event.accepted = true + TimelineManager.timeline.input.paste(false); + event.accepted = true; + } else if (event.matches(StandardKey.InsertParagraphSeparator)) { + TimelineManager.timeline.input.send(); + textArea.clear(); + event.accepted = true; } - else if (event.matches(StandardKey.InsertParagraphSeparator)) { - TimelineManager.timeline.input.send() - textArea.clear() - event.accepted = true + } + + Connections { + function onInsertText(text_) { + textArea.insert(textArea.cursorPosition, text_); } + + target: TimelineManager.timeline.input } MouseArea { @@ -110,6 +123,10 @@ Rectangle { Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.rightMargin: 16 + onClicked: { + TimelineManager.timeline.input.send(); + textArea.clear(); + } } } diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml new file mode 100644 index 00000000..8889989a --- /dev/null +++ b/resources/qml/NhekoBusyIndicator.qml @@ -0,0 +1,64 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 + +BusyIndicator { + id: control + + contentItem: Item { + implicitWidth: Math.min(parent.height, parent.width) + implicitHeight: implicitWidth + + Item { + id: item + + height: Math.min(parent.height, parent.width) + width: height + opacity: control.running ? 1 : 0 + + RotationAnimator { + target: item + running: control.visible && control.running + from: 0 + to: 360 + loops: Animation.Infinite + duration: 2000 + } + + Repeater { + id: repeater + + model: 6 + + Rectangle { + implicitWidth: radius * 2 + implicitHeight: radius * 2 + radius: item.height / 6 + color: colors.text + opacity: (index + 2) / (repeater.count + 2) + transform: [ + Translate { + y: -Math.min(item.width, item.height) * 0.5 + item.height / 6 + }, + Rotation { + angle: index / repeater.count * 360 + origin.x: item.height / 2 + origin.y: item.height / 2 + } + ] + } + + } + + Behavior on opacity { + OpacityAnimator { + duration: 250 + } + + } + + } + + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d85167af..5fce0846 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -192,13 +192,15 @@ Page { StackLayout { id: stackLayout + currentIndex: 0 Connections { - target: TimelineManager function onActiveTimelineChanged() { stackLayout.currentIndex = 0; } + + target: TimelineManager } MessageView { @@ -210,6 +212,7 @@ Page { source: TimelineManager.onVideoCall ? "VideoCall.qml" : "" onLoaded: TimelineManager.setVideoCallItem() } + } TypingIndicator { diff --git a/resources/qml/VideoCall.qml b/resources/qml/VideoCall.qml index 69fc1a2b..14408b6e 100644 --- a/resources/qml/VideoCall.qml +++ b/resources/qml/VideoCall.qml @@ -1,7 +1,6 @@ import QtQuick 2.9 - import org.freedesktop.gstreamer.GLVideoItem 1.0 GstGLVideoItem { - objectName: "videoCallItem" + objectName: "videoCallItem" } diff --git a/resources/res.qrc b/resources/res.qrc index efb9c907..02f31498 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -132,6 +132,7 @@ qml/Avatar.qml qml/ImageButton.qml qml/MatrixText.qml + qml/NhekoBusyIndicator.qml qml/StatusIndicator.qml qml/EncryptionIndicator.qml qml/Reactions.qml diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 6e1ed8ca..e09041e7 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -268,126 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect( - text_input_, - &TextInputWidget::uploadMedia, - this, - [this](QSharedPointer dev, QString mimeClass, const QString &fn) { - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(bin); - - auto payload = std::string(bin.data(), bin.size()); - std::optional encryptedFile; - if (cache::isRoomEncrypted(current_room_.toStdString())) { - mtx::crypto::BinaryBuf buf; - std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); - payload = mtx::crypto::to_string(buf); - } - - QSize dimensions; - QString blurhash; - if (mimeClass == "image") { - QImage img = utils::readImage(&bin); - - dimensions = img.size(); - if (img.height() > 200 && img.width() > 360) - img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); - std::vector data; - for (int y = 0; y < img.height(); y++) { - for (int x = 0; x < img.width(); x++) { - auto p = img.pixel(x, y); - data.push_back(static_cast(qRed(p))); - data.push_back(static_cast(qGreen(p))); - data.push_back(static_cast(qBlue(p))); - } - } - blurhash = QString::fromStdString( - blurhash::encode(data.data(), img.width(), img.height(), 4, 3)); - } - - http::client()->upload( - payload, - encryptedFile ? "application/octet-stream" : mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - encryptedFile, - mimeClass, - mime = mime.name(), - size = payload.size(), - dimensions, - blurhash](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload media. Please try again.")); - nhlog::net()->warn("failed to upload media: {} {} ({})", - err->matrix_error.error, - to_string(err->matrix_error.errcode), - static_cast(err->status_code)); - return; - } - - emit mediaUploaded(room_id, - filename, - encryptedFile, - QString::fromStdString(res.content_uri), - mimeClass, - mime, - size, - dimensions, - blurhash); - }); - }); - - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { - text_input_->hideUploadSpinner(); - emit showNotification(msg); - }); - connect(this, - &ChatPage::mediaUploaded, - this, - [this](QString roomid, - QString filename, - std::optional encryptedFile, - QString url, - QString mimeClass, - QString mime, - qint64 dsize, - QSize dimensions, - QString blurhash) { - text_input_->hideUploadSpinner(); - - if (encryptedFile) - encryptedFile->url = url.toStdString(); - - if (mimeClass == "image") - view_manager_->queueImageMessage(roomid, - filename, - encryptedFile, - url, - mime, - dsize, - dimensions, - blurhash); - else if (mimeClass == "audio") - view_manager_->queueAudioMessage( - roomid, filename, encryptedFile, url, mime, dsize); - else if (mimeClass == "video") - view_manager_->queueVideoMessage( - roomid, filename, encryptedFile, url, mime, dsize); - else - view_manager_->queueFileMessage( - roomid, filename, encryptedFile, url, mime, dsize); - }); - connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() { if (callManager_->onActiveCall()) { callManager_->hangUp(); diff --git a/src/ChatPage.h b/src/ChatPage.h index 9a38fd6e..41c6f276 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -126,17 +126,6 @@ signals: void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos); - void uploadFailed(const QString &msg); - void mediaUploaded(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mimeClass, - const QString &mime, - qint64 dsize, - const QSize &dimensions, - const QString &blurhash); - void contentLoaded(); void closing(); void changeWindowTitle(const int); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index dec7a574..232c0cad 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -88,10 +88,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) typingTimer_->setSingleShot(true); connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); - connect(&previewDialog_, - &dialogs::PreviewUploadOverlay::confirmUpload, - this, - &FilteredTextEdit::uploadData); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); connect( @@ -355,81 +351,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } -bool -FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const -{ - return (source->hasImage() || QTextEdit::canInsertFromMimeData(source)); -} - -void -FilteredTextEdit::insertFromMimeData(const QMimeData *source) -{ - qInfo() << "Got mime formats: \n" << source->formats(); - const auto formats = source->formats().filter("/"); - const auto image = formats.filter("image/", Qt::CaseInsensitive); - const auto audio = formats.filter("audio/", Qt::CaseInsensitive); - const auto video = formats.filter("video/", Qt::CaseInsensitive); - - if (!image.empty() && source->hasImage()) { - QImage img = qvariant_cast(source->imageData()); - previewDialog_.setPreview(img, image.front()); - } else if (!audio.empty()) { - showPreview(source, audio); - } else if (!video.empty()) { - showPreview(source, video); - } else if (source->hasUrls()) { - // Generic file path for any platform. - QString path; - for (auto &&u : source->urls()) { - if (u.isLocalFile()) { - path = u.toLocalFile(); - break; - } - } - - if (!path.isEmpty() && QFileInfo{path}.exists()) { - previewDialog_.setPreview(path); - } else { - qWarning() - << "Clipboard does not contain any valid file paths:" << source->urls(); - } - } else if (source->hasFormat("x-special/gnome-copied-files")) { - // Special case for X11 users. See "Notes for X11 Users" in source. - // Source: http://doc.qt.io/qt-5/qclipboard.html - - // This MIME type returns a string with multiple lines separated by '\n'. The first - // line is the command to perform with the clipboard (not useful to us). The - // following lines are the file URIs. - // - // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function - // nautilus_clipboard_get_uri_list_from_selection_data() - // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c - - auto data = source->data("x-special/gnome-copied-files").split('\n'); - if (data.size() < 2) { - qWarning() << "MIME format is malformed, cannot perform paste."; - return; - } - - QString path; - for (int i = 1; i < data.size(); ++i) { - QUrl url{data[i]}; - if (url.isLocalFile()) { - path = url.toLocalFile(); - break; - } - } - - if (!path.isEmpty()) { - previewDialog_.setPreview(path); - } else { - qWarning() << "Clipboard does not contain any valid file paths:" << data; - } - } else { - QTextEdit::insertFromMimeData(source); - } -} - void FilteredTextEdit::stopTyping() { @@ -494,28 +415,6 @@ FilteredTextEdit::textChanged() working_history_[history_index_] = toPlainText(); } -void -FilteredTextEdit::uploadData(const QByteArray data, - const QString &mediaType, - const QString &filename) -{ - QSharedPointer buffer{new QBuffer{this}}; - buffer->setData(data); - - emit startedUpload(); - - emit media(buffer, mediaType, filename); -} - -void -FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats) -{ - // Retrieve data as MIME type. - auto const &mime = formats.first(); - QByteArray data = source->data(mime); - previewDialog_.setPreview(data, mime); -} - TextInputWidget::TextInputWidget(QWidget *parent) : QWidget(parent) { @@ -624,7 +523,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) #endif connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -633,9 +531,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); - - connect( - input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner); } void @@ -654,47 +549,6 @@ TextInputWidget::addSelectedEmoji(const QString &emoji) input_->show(); } -void -TextInputWidget::openFileSelection() -{ - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)")); - - if (fileName.isEmpty()) - return; - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - - const auto format = mime.name().split("/")[0]; - - QSharedPointer file{new QFile{fileName, this}}; - - emit uploadMedia(file, format, QFileInfo(fileName).fileName()); - - showUploadSpinner(); -} - -void -TextInputWidget::showUploadSpinner() -{ - topLayout_->removeWidget(sendFileBtn_); - sendFileBtn_->hide(); - - topLayout_->insertWidget(1, spinner_); - spinner_->start(); -} - -void -TextInputWidget::hideUploadSpinner() -{ - topLayout_->removeWidget(spinner_); - topLayout_->insertWidget(1, sendFileBtn_); - sendFileBtn_->show(); - spinner_->stop(); -} - void TextInputWidget::stopTyping() { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index f9d84871..44419547 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -57,9 +57,6 @@ signals: void startedTyping(); void stoppedTyping(); void startedUpload(); - void message(QString msg); - void command(QString name, QString args); - void media(QSharedPointer data, QString mimeClass, const QString &filename); //! Trigger the suggestion popup. void showSuggestions(const QString &query); @@ -73,8 +70,6 @@ public slots: protected: void keyPressEvent(QKeyEvent *event) override; - bool canInsertFromMimeData(const QMimeData *source) const override; - void insertFromMimeData(const QMimeData *source) override; void focusOutEvent(QFocusEvent *event) override { suggestionsPopup_.hide(); @@ -131,9 +126,7 @@ private: void insertCompletion(QString completion); void textChanged(); - void uploadData(const QByteArray data, const QString &media, const QString &filename); void afterCompletion(int); - void showPreview(const QMimeData *source, const QStringList &formats); }; class TextInputWidget : public QWidget @@ -161,8 +154,6 @@ public: } public slots: - void openFileSelection(); - void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } void changeCallButtonState(webrtc::State); @@ -172,9 +163,6 @@ private slots: signals: void heightChanged(int height); - void uploadMedia(const QSharedPointer data, - QString mimeClass, - const QString &filename); void callButtonPress(); void sendJoinRoomRequest(const QString &room); @@ -192,8 +180,6 @@ protected: void paintEvent(QPaintEvent *) override; private: - void showUploadSpinner(); - QHBoxLayout *topLayout_; FilteredTextEdit *input_; diff --git a/src/Utils.cpp b/src/Utils.cpp index 38dbba22..2896e773 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -677,9 +677,10 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } QImage -utils::readImage(QByteArray *data) +utils::readImage(const QByteArray *data) { - QBuffer buf(data); + QBuffer buf; + buf.setData(*data); QImageReader reader(&buf); reader.setAutoTransform(true); return reader.read(); diff --git a/src/Utils.h b/src/Utils.h index 5e7fb601..f59e8673 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -303,5 +303,5 @@ restoreCombobox(QComboBox *combo, const QString &value); //! Read image respecting exif orientation QImage -readImage(QByteArray *data); +readImage(const QByteArray *data); } diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp index 20959b0a..e03993c7 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp @@ -60,7 +60,10 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent) emit confirmUpload(data_, mediaType_, fileName_.text()); close(); }); - connect(&cancel_, &QPushButton::clicked, this, &PreviewUploadOverlay::close); + connect(&cancel_, &QPushButton::clicked, this, [this]() { + emit aborted(); + close(); + }); } void diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h index 11cd49bc..5139e3f2 100644 --- a/src/dialogs/PreviewUploadOverlay.h +++ b/src/dialogs/PreviewUploadOverlay.h @@ -40,6 +40,7 @@ public: signals: void confirmUpload(const QByteArray data, const QString &media, const QString &filename); + void aborted(); private: void init(); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index dcd4a106..bd8f6414 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -1,18 +1,27 @@ #include "InputBar.h" #include +#include #include #include +#include +#include +#include #include +#include #include "Cache.h" #include "ChatPage.h" #include "Logging.h" #include "MatrixClient.h" +#include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "dialogs/PreviewUploadOverlay.h" + +#include "blurhash.hpp" static constexpr size_t INPUT_HISTORY_SIZE = 10; @@ -32,7 +41,66 @@ InputBar::paste(bool fromMouse) if (!md) return; - if (md->hasImage()) { + nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString()); + const auto formats = md->formats().filter("/"); + const auto image = formats.filter("image/", Qt::CaseInsensitive); + const auto audio = formats.filter("audio/", Qt::CaseInsensitive); + const auto video = formats.filter("video/", Qt::CaseInsensitive); + + if (!image.empty() && md->hasImage()) { + showPreview(*md, "", image); + } else if (!audio.empty()) { + showPreview(*md, "", audio); + } else if (!video.empty()) { + showPreview(*md, "", video); + } else if (md->hasUrls()) { + // Generic file path for any platform. + QString path; + for (auto &&u : md->urls()) { + if (u.isLocalFile()) { + path = u.toLocalFile(); + break; + } + } + + if (!path.isEmpty() && QFileInfo{path}.exists()) { + showPreview(*md, path, formats); + } else { + nhlog::ui()->warn("Clipboard does not contain any valid file paths."); + } + } else if (md->hasFormat("x-special/gnome-copied-files")) { + // Special case for X11 users. See "Notes for X11 Users" in md. + // Source: http://doc.qt.io/qt-5/qclipboard.html + + // This MIME type returns a string with multiple lines separated by '\n'. The first + // line is the command to perform with the clipboard (not useful to us). The + // following lines are the file URIs. + // + // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function + // nautilus_clipboard_get_uri_list_from_selection_data() + // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c + + auto data = md->data("x-special/gnome-copied-files").split('\n'); + if (data.size() < 2) { + nhlog::ui()->warn("MIME format is malformed, cannot perform paste."); + return; + } + + QString path; + for (int i = 1; i < data.size(); ++i) { + QUrl url{data[i]}; + if (url.isLocalFile()) { + path = url.toLocalFile(); + break; + } + } + + if (!path.isEmpty()) { + showPreview(*md, path, formats); + } else { + nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}", + data.join(", ").toStdString()); + } } else if (md->hasText()) { emit insertText(md->text()); } else { @@ -78,6 +146,37 @@ InputBar::send() nhlog::ui()->debug("Send: {}", text.toStdString()); } +void +InputBar::openFileSelection() +{ + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const auto fileName = QFileDialog::getOpenFileName( + ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + QFile file{fileName}; + + if (!file.open(QIODevice::ReadOnly)) { + emit ChatPage::instance()->showNotification( + QString("Error while reading media: %1").arg(file.errorString())); + return; + } + + setUploading(true); + + auto bin = file.readAll(); + + QMimeData data; + data.setData(mime.name(), bin); + + showPreview(data, fileName, QStringList{mime.name()}); +} + void InputBar::message(QString msg) { @@ -149,6 +248,112 @@ InputBar::emote(QString msg) room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); } +void +InputBar::image(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const QString &blurhash) +{ + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.info.blurhash = blurhash.toStdString(); + image.body = filename.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + + if (file) + image.file = file; + else + image.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + image.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); +} + +void +InputBar::file(const QString &filename, + const std::optional &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + + if (encryptedFile) + file.file = encryptedFile; + else + file.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + file.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); +} + +void +InputBar::audio(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + + if (file) + audio.file = file; + else + audio.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + audio.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); +} + +void +InputBar::video(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize) +{ + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + + if (file) + video.file = file; + else + video.url = url.toStdString(); + + if (!room->reply().isEmpty()) { + video.relates_to.in_reply_to.event_id = room->reply().toStdString(); + room->resetReply(); + } + + room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); +} + void InputBar::command(QString command, QString args) { @@ -196,3 +401,113 @@ InputBar::command(QString command, QString args) cache::dropOutboundMegolmSession(room->roomId().toStdString()); } } + +void +InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats) +{ + dialogs::PreviewUploadOverlay *previewDialog_ = + new dialogs::PreviewUploadOverlay(ChatPage::instance()); + previewDialog_->setAttribute(Qt::WA_DeleteOnClose); + + if (source.hasImage()) + previewDialog_->setPreview(qvariant_cast(source.imageData()), + formats.front()); + else if (!path.isEmpty()) + previewDialog_->setPreview(path); + else if (!formats.isEmpty()) { + auto mime = formats.first(); + previewDialog_->setPreview(source.data(mime), mime); + } else { + setUploading(false); + previewDialog_->deleteLater(); + return; + } + + connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() { + setUploading(false); + }); + + connect( + previewDialog_, + &dialogs::PreviewUploadOverlay::confirmUpload, + this, + [this](const QByteArray data, const QString &mime, const QString &fn) { + setUploading(true); + + auto payload = std::string(data.data(), data.size()); + std::optional encryptedFile; + if (cache::isRoomEncrypted(room->roomId().toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + QString blurhash; + auto mimeClass = mime.split("/")[0]; + if (mimeClass == "image") { + QImage img = utils::readImage(&data); + + dimensions = img.size(); + if (img.height() > 200 && img.width() > 360) + img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); + std::vector data; + for (int y = 0; y < img.height(); y++) { + for (int x = 0; x < img.width(); x++) { + auto p = img.pixel(x, y); + data.push_back(static_cast(qRed(p))); + data.push_back(static_cast(qGreen(p))); + data.push_back(static_cast(qBlue(p))); + } + } + blurhash = QString::fromStdString( + blurhash::encode(data.data(), img.width(), img.height(), 4, 3)); + } + + http::client()->upload( + payload, + encryptedFile ? "application/octet-stream" : mime.toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + filename = fn, + encryptedFile = std::move(encryptedFile), + mimeClass, + mime, + size = payload.size(), + dimensions, + blurhash](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) mutable { + if (err) { + emit ChatPage::instance()->showNotification( + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", + err->matrix_error.error, + to_string(err->matrix_error.errcode), + static_cast(err->status_code)); + setUploading(false); + return; + } + + auto url = QString::fromStdString(res.content_uri); + if (encryptedFile) + encryptedFile->url = res.content_uri; + + if (mimeClass == "image") + image(filename, + encryptedFile, + url, + mime, + size, + dimensions, + blurhash); + else if (mimeClass == "audio") + audio(filename, encryptedFile, url, mime, size); + else if (mimeClass == "video") + video(filename, encryptedFile, url, mime, size); + else + file(filename, encryptedFile, url, mime, size); + + setUploading(false); + }); + }); +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index f3a38c2e..35e3f8a4 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -3,11 +3,17 @@ #include #include +#include +#include + class TimelineModel; +class QMimeData; +class QStringList; class InputBar : public QObject { Q_OBJECT + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) public: InputBar(TimelineModel *parent) @@ -19,18 +25,53 @@ public slots: void send(); void paste(bool fromMouse); void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); + void openFileSelection(); + bool uploading() const { return uploading_; } signals: void insertText(QString text); + void uploadingChanged(bool value); private: void message(QString body); void emote(QString body); void command(QString name, QString args); + void image(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const QString &blurhash); + void file(const QString &filename, + const std::optional &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize); + void audio(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize); + void video(const QString &filename, + const std::optional &file, + const QString &url, + const QString &mime, + uint64_t dsize); + + void showPreview(const QMimeData &source, QString path, const QStringList &formats); + void setUploading(bool value) + { + if (value != uploading_) { + uploading_ = value; + emit uploadingChanged(value); + } + } TimelineModel *room; QString text; std::deque history_; std::size_t history_index_ = 0; int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; + bool uploading_ = false; }; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 58a1496c..16a2565e 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -150,7 +150,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) - Q_PROPERTY(InputBar *input READ input) + Q_PROPERTY(InputBar *input READ input CONSTANT) public: explicit TimelineModel(TimelineViewManager *manager, -- cgit 1.5.1 From d1af1a86691290c67ad9e2b6ae9440b1254a2c0b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 15 Nov 2020 23:14:47 +0100 Subject: Move calls to new input bar --- resources/qml/MessageInput.qml | 13 +++- resources/qml/NhekoBusyIndicator.qml | 4 +- src/ChatPage.cpp | 32 --------- src/ChatPage.h | 1 + src/TextInputWidget.cpp | 30 -------- src/TextInputWidget.h | 5 -- src/timeline/InputBar.cpp | 45 ++++++++++++ src/timeline/InputBar.h | 1 + src/timeline/TimelineViewManager.cpp | 136 ++++++----------------------------- src/timeline/TimelineViewManager.h | 31 ++------ 10 files changed, 86 insertions(+), 212 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index a1220599..17e1198d 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -17,14 +17,18 @@ Rectangle { spacing: 16 ImageButton { + visible: TimelineManager.callsSupported Layout.alignment: Qt.AlignBottom hoverEnabled: true width: 22 height: 22 - image: ":/icons/icons/ui/place-call.png" + image: TimelineManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png" + ToolTip.visible: hovered + ToolTip.text: TimelineManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.leftMargin: 16 + onClicked: TimelineManager.timeline.input.callButton() } ImageButton { @@ -35,7 +39,10 @@ Rectangle { image: ":/icons/icons/ui/paper-clip-outline.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + Layout.leftMargin: TimelineManager.callsSupported ? 0 : 16 onClicked: TimelineManager.timeline.input.openFileSelection() + ToolTip.visible: hovered + ToolTip.text: qsTr("Send a file") Rectangle { anchors.fill: parent @@ -112,6 +119,8 @@ Rectangle { image: ":/icons/icons/ui/smile.png" Layout.topMargin: 8 Layout.bottomMargin: 8 + ToolTip.visible: hovered + ToolTip.text: qsTr("Emoji") } ImageButton { @@ -123,6 +132,8 @@ Rectangle { Layout.topMargin: 8 Layout.bottomMargin: 8 Layout.rightMargin: 16 + ToolTip.visible: hovered + ToolTip.text: qsTr("Send") onClicked: { TimelineManager.timeline.input.send(); textArea.clear(); diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml index 8889989a..89a40dd5 100644 --- a/resources/qml/NhekoBusyIndicator.qml +++ b/resources/qml/NhekoBusyIndicator.qml @@ -6,8 +6,8 @@ BusyIndicator { id: control contentItem: Item { - implicitWidth: Math.min(parent.height, parent.width) - implicitHeight: implicitWidth + implicitWidth: 64 + implicitHeight: 64 Item { id: item diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index e09041e7..c703d95f 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -268,38 +268,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, SIGNAL(unreadMessages(int))); - connect(text_input_, &TextInputWidget::callButtonPress, this, [this]() { - if (callManager_->onActiveCall()) { - callManager_->hangUp(); - } else { - if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString()); - roomInfo.member_count != 2) { - showNotification("Calls are limited to 1:1 rooms."); - } else { - std::vector members( - cache::getMembers(current_room_.toStdString())); - const RoomMember &callee = - members.front().user_id == utils::localUser() ? members.back() - : members.front(); - auto dialog = new dialogs::PlaceCall( - callee.user_id, - callee.display_name, - QString::fromStdString(roomInfo.name), - QString::fromStdString(roomInfo.avatar_url), - userSettings_, - MainWindow::instance()); - connect(dialog, &dialogs::PlaceCall::voice, this, [this]() { - callManager_->sendInvite(current_room_, false); - }); - connect(dialog, &dialogs::PlaceCall::video, this, [this]() { - callManager_->sendInvite(current_room_, true); - }); - utils::centerWidget(dialog, MainWindow::instance()); - dialog->show(); - } - } - }); - connect( this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); diff --git a/src/ChatPage.h b/src/ChatPage.h index 41c6f276..7eb37f04 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -88,6 +88,7 @@ public: static ChatPage *instance() { return instance_; } QSharedPointer userSettings() { return userSettings_; } + CallManager *callManager() { return callManager_; } void deleteConfigs(); CommunitiesList *communitiesList() { return communitiesList_; } diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 232c0cad..1a856abb 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -431,15 +431,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) topLayout_->setSpacing(0); topLayout_->setContentsMargins(13, 1, 13, 0); -#ifdef GSTREAMER_AVAILABLE - callBtn_ = new FlatButton(this); - changeCallButtonState(webrtc::State::DISCONNECTED); - connect(&WebRTCSession::instance(), - &WebRTCSession::stateChanged, - this, - &TextInputWidget::changeCallButtonState); -#endif - QIcon send_file_icon; send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); @@ -508,9 +499,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) emojiBtn_->setIcon(emoji_icon); emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); -#ifdef GSTREAMER_AVAILABLE - topLayout_->addWidget(callBtn_); -#endif topLayout_->addWidget(sendFileBtn_); topLayout_->addWidget(input_); topLayout_->addWidget(emojiBtn_); @@ -518,9 +506,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) setLayout(topLayout_); -#ifdef GSTREAMER_AVAILABLE - connect(callBtn_, &FlatButton::clicked, this, &TextInputWidget::callButtonPress); -#endif connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); connect(emojiBtn_, @@ -570,18 +555,3 @@ TextInputWidget::paintEvent(QPaintEvent *) style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } - -void -TextInputWidget::changeCallButtonState(webrtc::State state) -{ - QIcon icon; - if (state == webrtc::State::ICEFAILED || state == webrtc::State::DISCONNECTED) { - callBtn_->setToolTip(tr("Place a call")); - icon.addFile(":/icons/icons/ui/place-call.png"); - } else { - callBtn_->setToolTip(tr("Hang up")); - icon.addFile(":/icons/icons/ui/end-call.png"); - } - callBtn_->setIcon(icon); - callBtn_->setIconSize(QSize(ButtonHeight * 1.1, ButtonHeight * 1.1)); -} diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 44419547..9613f209 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -26,7 +26,6 @@ #include #include -#include "WebRTCSession.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" #include "popups/SuggestionsPopup.h" @@ -155,7 +154,6 @@ public: public slots: void focusLineEdit() { input_->setFocus(); } - void changeCallButtonState(webrtc::State); private slots: void addSelectedEmoji(const QString &emoji); @@ -163,8 +161,6 @@ private slots: signals: void heightChanged(int height); - void callButtonPress(); - void sendJoinRoomRequest(const QString &room); void sendInviteRoomRequest(const QString &userid, const QString &reason); void sendKickRoomRequest(const QString &userid, const QString &reason); @@ -185,7 +181,6 @@ private: LoadingIndicator *spinner_; - FlatButton *callBtn_; FlatButton *sendFileBtn_; FlatButton *sendMessageBtn_; emoji::PickButton *emojiBtn_; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index bd8f6414..dc287f94 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -12,13 +12,16 @@ #include #include "Cache.h" +#include "CallManager.h" #include "ChatPage.h" #include "Logging.h" +#include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" #include "TimelineModel.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "dialogs/PlaceCall.h" #include "dialogs/PreviewUploadOverlay.h" #include "blurhash.hpp" @@ -511,3 +514,45 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & }); }); } + +void +InputBar::callButton() +{ + auto callManager_ = ChatPage::instance()->callManager(); + if (callManager_->onActiveCall()) { + callManager_->hangUp(); + } else { + auto current_room_ = room->roomId(); + if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString()); + roomInfo.member_count != 2) { + ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms."); + } else { + std::vector members( + cache::getMembers(current_room_.toStdString())); + const RoomMember &callee = members.front().user_id == utils::localUser() + ? members.back() + : members.front(); + auto dialog = + new dialogs::PlaceCall(callee.user_id, + callee.display_name, + QString::fromStdString(roomInfo.name), + QString::fromStdString(roomInfo.avatar_url), + ChatPage::instance()->userSettings(), + MainWindow::instance()); + connect(dialog, + &dialogs::PlaceCall::voice, + callManager_, + [callManager_, current_room_]() { + callManager_->sendInvite(current_room_, false); + }); + connect(dialog, + &dialogs::PlaceCall::video, + callManager_, + [callManager_, current_room_]() { + callManager_->sendInvite(current_room_, true); + }); + utils::centerWidget(dialog, MainWindow::instance()); + dialog->show(); + } + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index 35e3f8a4..a52a3904 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -27,6 +27,7 @@ public slots: void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); void openFileSelection(); bool uploading() const { return uploading_; } + void callButton(); signals: void insertText(QString text); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index f949498d..1392c505 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -244,6 +244,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par &CallManager::newVideoCallState, this, &TimelineViewManager::videoCallChanged); + + connect(&WebRTCSession::instance(), + &WebRTCSession::stateChanged, + this, + &TimelineViewManager::onCallChanged); +} + +bool +TimelineViewManager::isOnCall() const +{ + return callManager_->onActiveCall(); +} +bool +TimelineViewManager::callsSupported() const +{ +#ifdef GSTREAMER_AVAILABLE + return true; +#else + return false; +#endif } void @@ -506,122 +526,6 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt timeline_->redactEvent(selfReactedEvent); } } - -void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions, - const QString &blurhash) -{ - mtx::events::msg::Image image; - image.info.mimetype = mime.toStdString(); - image.info.size = dsize; - image.info.blurhash = blurhash.toStdString(); - image.body = filename.toStdString(); - image.info.h = dimensions.height(); - image.info.w = dimensions.width(); - - if (file) - image.file = file; - else - image.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - image.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(image, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueFileMessage( - const QString &roomid, - const QString &filename, - const std::optional &encryptedFile, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::File file; - file.info.mimetype = mime.toStdString(); - file.info.size = dsize; - file.body = filename.toStdString(); - - if (encryptedFile) - file.file = encryptedFile; - else - file.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - file.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(file, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = mime.toStdString(); - audio.info.size = dsize; - audio.body = filename.toStdString(); - audio.url = url.toStdString(); - - if (file) - audio.file = file; - else - audio.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - audio.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); -} - -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Video video; - video.info.mimetype = mime.toStdString(); - video.info.size = dsize; - video.body = filename.toStdString(); - - if (file) - video.file = file; - else - video.url = url.toStdString(); - - auto model = models.value(roomid); - if (!model->reply().isEmpty()) { - video.relates_to.in_reply_to.event_id = model->reply().toStdString(); - model->resetReply(); - } - - model->sendMessageEvent(video, mtx::events::EventType::RoomMessage); -} - void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 02e0e132..371e9af2 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -41,6 +41,8 @@ class TimelineViewManager : public QObject Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged) Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged) Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) + Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY onCallChanged) + Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT) public: TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); @@ -95,6 +97,7 @@ signals: void videoCallChanged(); void callPartyChanged(); void micMuteChanged(); + void onCallChanged(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); @@ -104,38 +107,14 @@ public slots: void setHistoryView(const QString &room_id); void updateColorPalette(); void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); - void queueImageMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions, - const QString &blurhash); - void queueFileMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const std::optional &file, - const QString &url, - const QString &mime, - uint64_t dsize); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); void updateEncryptedDescriptions(); + bool isOnCall() const; + bool callsSupported() const; void enableBackButton() { -- cgit 1.5.1 From 921379a4cc3bc6381c4562bfdb9ce0dcde6b1f2c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 17 Nov 2020 02:37:43 +0100 Subject: Send typing updates from QML --- src/ChatPage.cpp | 47 ------------------------------------------ src/ChatPage.h | 4 ---- src/TextInputWidget.cpp | 30 --------------------------- src/TextInputWidget.h | 10 --------- src/timeline/InputBar.cpp | 41 ++++++++++++++++++++++++++++++++++++ src/timeline/InputBar.h | 16 +++++++++++++- src/timeline/TimelineModel.cpp | 1 - 7 files changed, 56 insertions(+), 93 deletions(-) (limited to 'src/ChatPage.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c703d95f..1b235a95 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -141,9 +141,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) text_input_ = new TextInputWidget(this); contentLayout_->addWidget(text_input_); - typingRefresher_ = new QTimer(this); - typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); - connect(this, &ChatPage::connectionLost, this, [this]() { nhlog::net()->info("connectivity lost"); isConnected_ = false; @@ -221,9 +218,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) { this->current_room_ = room_id; }); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit); connect( room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); @@ -237,27 +232,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) room_list_->removeRoom(room_id, currentRoom() == room_id); }); - connect( - text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); - connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); - connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { - if (!userSettings_->typingNotifications()) - return; - - typingRefresher_->stop(); - - if (current_room_.isEmpty()) - return; - - http::client()->stop_typing( - current_room_.toStdString(), [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to stop typing notifications: {}", - err->matrix_error.error); - } - }); - }); - connect(view_manager_, &TimelineViewManager::updateRoomsLastMessage, room_list_, @@ -435,12 +409,6 @@ ChatPage::resetUI() emit unreadMessages(0); } -void -ChatPage::focusMessageInput() -{ - this->text_input_->focusLineEdit(); -} - void ChatPage::deleteConfigs() { @@ -1099,21 +1067,6 @@ ChatPage::receivedSessionKey(const std::string &room_id, const std::string &sess view_manager_->receivedSessionKey(room_id, session_id); } -void -ChatPage::sendTypingNotifications() -{ - if (!userSettings_->typingNotifications()) - return; - - http::client()->start_typing( - current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send typing notification: {}", - err->matrix_error.error); - } - }); -} - QString ChatPage::status() const { diff --git a/src/ChatPage.h b/src/ChatPage.h index 7eb37f04..37abafa0 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -100,7 +100,6 @@ public: //! Show the room/group list (if it was visible). void showSideBars(); void initiateLogout(); - void focusMessageInput(); QString status() const; void setStatus(const QString &status); @@ -191,7 +190,6 @@ private slots: void removeRoom(const QString &room_id); void dropToLoginPage(const QString &msg); - void sendTypingNotifications(); void handleSyncResponse(const mtx::responses::Sync &res); private: @@ -265,8 +263,6 @@ private: popups::UserMentions *user_mentions_popup_; - QTimer *typingRefresher_; - // Global user settings. QSharedPointer userSettings_; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 92449231..30589b61 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -83,12 +83,6 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) insertCompletion(emoji); }); - typingTimer_ = new QTimer(this); - typingTimer_->setInterval(1000); - typingTimer_->setSingleShot(true); - - connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); - connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); connect( &suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { @@ -164,13 +158,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_U) QTextEdit::setText(""); - if (!isModifier) { - if (!typingTimer_->isActive()) - emit startedTyping(); - - typingTimer_->start(); - } - // calculate the new query if (textCursor().position() < atTriggerPosition_ || !isAnchorValid()) { resetAnchor(); @@ -264,7 +251,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } if (!(event->modifiers() & Qt::ShiftModifier)) { - stopTyping(); submit(); } else { QTextEdit::keyPressEvent(event); @@ -351,13 +337,6 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } -void -FilteredTextEdit::stopTyping() -{ - typingTimer_->stop(); - emit stoppedTyping(); -} - QRect FilteredTextEdit::completerRect() { @@ -494,15 +473,6 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); - - connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); -} - -void -TextInputWidget::stopTyping() -{ - input_->stopTyping(); } void diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index afd29439..c62a98be 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -43,8 +43,6 @@ class FilteredTextEdit : public QTextEdit public: explicit FilteredTextEdit(QWidget *parent = nullptr); - void stopTyping(); - QSize sizeHint() const override; QSize minimumSizeHint() const override; @@ -52,8 +50,6 @@ public: signals: void heightChanged(int height); - void startedTyping(); - void stoppedTyping(); void startedUpload(); //! Trigger the suggestion popup. @@ -81,7 +77,6 @@ private: int trigger_pos_; // Where emoji completer was triggered size_t history_index_; QCompleter *completer_; - QTimer *typingTimer_; SuggestionsPopup suggestionsPopup_; @@ -136,8 +131,6 @@ class TextInputWidget : public QWidget public: TextInputWidget(QWidget *parent = nullptr); - void stopTyping(); - QColor borderColor() const { return borderColor_; } void setBorderColor(QColor &color) { borderColor_ = color; } void disableInput() @@ -164,9 +157,6 @@ signals: void sendUnbanRoomRequest(const QString &userid, const QString &reason); void changeRoomNick(const QString &displayname); - void startedTyping(); - void stoppedTyping(); - protected: void focusInEvent(QFocusEvent *event) override; void paintEvent(QPaintEvent *) override; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index dc287f94..6603287b 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -114,6 +114,11 @@ InputBar::paste(bool fromMouse) void InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_) { + if (text_.isEmpty()) + stopTyping(); + else + startTyping(); + selectionStart = selectionStart_; selectionEnd = selectionEnd_; cursorPosition = cursorPosition_; @@ -556,3 +561,39 @@ InputBar::callButton() } } } + +void +InputBar::startTyping() +{ + if (!typingRefresh_.isActive()) { + typingRefresh_.start(); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + http::client()->start_typing( + room->roomId().toStdString(), 10'000, [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to send typing notification: {}", + err->matrix_error.error); + } + }); + } + } + typingTimeout_.start(); +} +void +InputBar::stopTyping() +{ + typingRefresh_.stop(); + typingTimeout_.stop(); + + if (!ChatPage::instance()->userSettings()->typingNotifications()) + return; + + http::client()->stop_typing(room->roomId().toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); + } + }); +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index a52a3904..0e9ef592 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -19,7 +20,14 @@ public: InputBar(TimelineModel *parent) : QObject() , room(parent) - {} + { + typingRefresh_.setInterval(10'000); + typingRefresh_.setSingleShot(true); + typingTimeout_.setInterval(5'000); + typingTimeout_.setSingleShot(true); + connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping); + connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); + } public slots: void send(); @@ -29,6 +37,10 @@ public slots: bool uploading() const { return uploading_; } void callButton(); +private slots: + void startTyping(); + void stopTyping(); + signals: void insertText(QString text); void uploadingChanged(bool value); @@ -69,6 +81,8 @@ private: } } + QTimer typingRefresh_; + QTimer typingTimeout_; TimelineModel *room; QString text; std::deque history_; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 8b80ea51..4cbd5777 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -800,7 +800,6 @@ void TimelineModel::replyAction(QString id) { setReply(id); - ChatPage::instance()->focusMessageInput(); } RelatedInfo -- cgit 1.5.1 From b47d2a809c533d74eddce67683c0cbda8ca71446 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 20 Nov 2020 04:36:13 +0100 Subject: Remove old Textinput --- CMakeLists.txt | 2 - src/ChatPage.cpp | 7 - src/ChatPage.h | 3 - src/TextInputWidget.cpp | 496 ------------------------------------------------ src/TextInputWidget.h | 173 ----------------- 5 files changed, 681 deletions(-) delete mode 100644 src/TextInputWidget.cpp delete mode 100644 src/TextInputWidget.h (limited to 'src/ChatPage.cpp') 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_; -}; -- cgit 1.5.1