Initial Support for Rich Replies

Add placeholder UI for showing replies in the text entry widget.
Existing quoting capability has been removed (Temporarily), as
it was replaced with the new reply capability.  Replies sent from
nheko do not currently appear correctly in the timeline (this
will be fixed in a future commit).
This commit is contained in:
Joseph Donofry 2019-06-11 21:04:30 -04:00
parent b9dde957a8
commit 9159b9ce22
No known key found for this signature in database
GPG key ID: E8A1D78EF044B0CB
11 changed files with 412 additions and 255 deletions

View file

@ -237,7 +237,9 @@ set(SRC_FILES
src/RunGuard.cpp
src/SideBarActions.cpp
src/Splitter.cpp
src/SuggestionsPopup.cpp
src/popups/SuggestionsPopup.cpp
src/popups/PopupItem.cpp
src/popups/ReplyPopup.cpp
src/TextInputWidget.cpp
src/TopRoomBar.cpp
src/TrayIcon.cpp
@ -375,7 +377,9 @@ qt5_wrap_cpp(MOC_HEADERS
src/RoomList.h
src/SideBarActions.h
src/Splitter.h
src/SuggestionsPopup.h
src/popups/SuggestionsPopup.h
src/popups/ReplyPopup.h
src/popups/PopupItem.h
src/TextInputWidget.h
src/TopRoomBar.h
src/TrayIcon.h

View file

@ -23,7 +23,7 @@
#include <QtConcurrent>
#include "QuickSwitcher.h"
#include "SuggestionsPopup.h"
#include "popups/SuggestionsPopup.h"
RoomSearchInput::RoomSearchInput(QWidget *parent)
: TextField(parent)

View file

@ -22,7 +22,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include "SuggestionsPopup.h"
#include "popups/SuggestionsPopup.h"
#include "ui/TextField.h"
Q_DECLARE_METATYPE(std::vector<RoomSearchResult>)

View file

@ -48,7 +48,8 @@ static constexpr int ButtonHeight = 22;
FilteredTextEdit::FilteredTextEdit(QWidget *parent)
: QTextEdit{parent}
, history_index_{0}
, popup_{parent}
, suggestionsPopup_{parent}
, replyPopup_{parent}
, previewDialog_{parent}
{
setFrameStyle(QFrame::NoFrame);
@ -75,8 +76,13 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
&FilteredTextEdit::uploadData);
connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults);
connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
popup_.hide();
connect(&replyPopup_, &ReplyPopup::userSelected, this, [this](const QString &text) {
// TODO: Show user avatar window.
nhlog::ui()->info("User selected: " + text.toStdString());
});
connect(
&suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) {
suggestionsPopup_.hide();
auto cursor = textCursor();
const int end = cursor.position();
@ -90,14 +96,14 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
// For cycling through the suggestions by hitting tab.
connect(this,
&FilteredTextEdit::selectNextSuggestion,
&popup_,
&suggestionsPopup_,
&SuggestionsPopup::selectNextSuggestion);
connect(this,
&FilteredTextEdit::selectPreviousSuggestion,
&popup_,
&suggestionsPopup_,
&SuggestionsPopup::selectPreviousSuggestion);
connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() {
popup_.selectHoveredSuggestion<UserItem>();
suggestionsPopup_.selectHoveredSuggestion<UserItem>();
});
previewDialog_.hide();
@ -117,9 +123,9 @@ FilteredTextEdit::showResults(const QVector<SearchResult> &results)
pos = viewport()->mapToGlobal(rect.topLeft());
}
popup_.addUsers(results);
popup_.move(pos.x(), pos.y() - popup_.height() - 10);
popup_.show();
suggestionsPopup_.addUsers(results);
suggestionsPopup_.move(pos.x(), pos.y() - suggestionsPopup_.height() - 10);
suggestionsPopup_.show();
}
void
@ -146,7 +152,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
closeSuggestions();
}
if (popup_.isVisible()) {
if (suggestionsPopup_.isVisible()) {
switch (event->key()) {
case Qt::Key_Down:
case Qt::Key_Tab:
@ -169,6 +175,19 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
}
}
if (replyPopup_.isVisible()) {
switch (event->key())
{
case Qt::Key_Escape:
closeReply();
return;
default:
break;
}
}
switch (event->key()) {
case Qt::Key_At:
atTriggerPosition_ = textCursor().position();
@ -419,6 +438,24 @@ FilteredTextEdit::submit()
clear();
}
void
FilteredTextEdit::showReplyPopup(const QString &user, const QString &msg, const QString &event_id)
{
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());
}
replyPopup_.setReplyContent(user, msg, event_id);
replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10);
replyPopup_.show();
}
void
FilteredTextEdit::textChanged()
{
@ -666,9 +703,10 @@ TextInputWidget::paintEvent(QPaintEvent *)
void
TextInputWidget::addReply(const QString &username, const QString &msg, const QString &replied_event)
{
input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg));
// input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg));
input_->setFocus();
input_->showReplyPopup(username, msg, replied_event);
auto cursor = input_->textCursor();
cursor.movePosition(QTextCursor::End);
input_->setTextCursor(cursor);

View file

@ -28,7 +28,8 @@
#include <QTextEdit>
#include <QWidget>
#include "SuggestionsPopup.h"
#include "popups/SuggestionsPopup.h"
#include "popups/ReplyPopup.h"
#include "dialogs/PreviewUploadOverlay.h"
#include "emoji/PickButton.h"
@ -55,6 +56,7 @@ public:
void submit();
void setRelatedEvent(const QString &event) { related_event_ = event; }
void showReplyPopup(const QString &user, const QString &msg, const QString &event_id);
signals:
void heightChanged(int height);
@ -85,7 +87,7 @@ protected:
void insertFromMimeData(const QMimeData *source) override;
void focusOutEvent(QFocusEvent *event) override
{
popup_.hide();
suggestionsPopup_.hide();
QTextEdit::focusOutEvent(event);
}
@ -94,7 +96,8 @@ private:
size_t history_index_;
QTimer *typingTimer_;
SuggestionsPopup popup_;
SuggestionsPopup suggestionsPopup_;
ReplyPopup replyPopup_;
// Used for replies
QString related_event_;
@ -109,7 +112,8 @@ private:
int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); }
void closeSuggestions() { popup_.hide(); }
void closeSuggestions() { suggestionsPopup_.hide(); }
void closeReply() { replyPopup_.hide(); }
void resetAnchor() { atTriggerPosition_ = -1; }
bool isAnchorValid() { return atTriggerPosition_ != -1; }
bool hasAnchor(int pos, AnchorType anchor)

View file

@ -2,11 +2,9 @@
#include <QPainter>
#include <QStyleOption>
#include "Config.h"
#include "SuggestionsPopup.h"
#include "Utils.h"
#include "ui/Avatar.h"
#include "ui/DropShadow.h"
#include "PopupItem.h"
#include "../Utils.h"
#include "../ui/Avatar.h"
constexpr int PopupHMargin = 4;
constexpr int PopupItemMargin = 3;
@ -147,150 +145,3 @@ RoomItem::mousePressEvent(QMouseEvent *event)
QWidget::mousePressEvent(event);
}
SuggestionsPopup::SuggestionsPopup(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_ShowWithoutActivating, true);
setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
layout_ = new QVBoxLayout(this);
layout_->setMargin(0);
layout_->setSpacing(0);
}
void
SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms)
{
if (rooms.empty()) {
hide();
return;
}
const size_t layoutCount = layout_->count();
const size_t roomCount = rooms.size();
// Remove the extra widgets from the layout.
if (roomCount < layoutCount)
removeLayoutItemsAfter(roomCount - 1);
for (size_t i = 0; i < roomCount; ++i) {
auto item = layout_->itemAt(i);
// Create a new widget if there isn't already one in that
// layout position.
if (!item) {
auto room = new RoomItem(this, rooms.at(i));
connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected);
layout_->addWidget(room);
} else {
// Update the current widget with the new data.
auto room = qobject_cast<RoomItem *>(item->widget());
if (room)
room->updateItem(rooms.at(i));
}
}
resetSelection();
adjustSize();
resize(geometry().width(), 40 * rooms.size());
selectNextSuggestion();
}
void
SuggestionsPopup::addUsers(const QVector<SearchResult> &users)
{
if (users.isEmpty()) {
hide();
return;
}
const size_t layoutCount = layout_->count();
const size_t userCount = users.size();
// Remove the extra widgets from the layout.
if (userCount < layoutCount)
removeLayoutItemsAfter(userCount - 1);
for (size_t i = 0; i < userCount; ++i) {
auto item = layout_->itemAt(i);
// Create a new widget if there isn't already one in that
// layout position.
if (!item) {
auto user = new UserItem(this, users.at(i).user_id);
connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected);
layout_->addWidget(user);
} else {
// Update the current widget with the new data.
auto userWidget = qobject_cast<UserItem *>(item->widget());
if (userWidget)
userWidget->updateItem(users.at(i).user_id);
}
}
resetSelection();
adjustSize();
selectNextSuggestion();
}
void
SuggestionsPopup::hoverSelection()
{
resetHovering();
setHovering(selectedItem_);
update();
}
void
SuggestionsPopup::selectNextSuggestion()
{
selectedItem_++;
if (selectedItem_ >= layout_->count())
selectFirstItem();
hoverSelection();
}
void
SuggestionsPopup::selectPreviousSuggestion()
{
selectedItem_--;
if (selectedItem_ < 0)
selectLastItem();
hoverSelection();
}
void
SuggestionsPopup::resetHovering()
{
for (int i = 0; i < layout_->count(); ++i) {
const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget());
if (item)
item->setHovering(false);
}
}
void
SuggestionsPopup::setHovering(int pos)
{
const auto &item = layout_->itemAt(pos);
const auto &widget = qobject_cast<PopupItem *>(item->widget());
if (widget)
widget->setHovering(true);
}
void
SuggestionsPopup::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

83
src/popups/PopupItem.h Normal file
View file

@ -0,0 +1,83 @@
#pragma once
#include <QHBoxLayout>
#include <QLabel>
#include <QPoint>
#include <QWidget>
#include "../AvatarProvider.h"
#include "../Cache.h"
#include "../ChatPage.h"
class Avatar;
struct SearchResult;
class PopupItem : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
Q_PROPERTY(bool hovering READ hovering WRITE setHovering)
public:
PopupItem(QWidget *parent);
QString selectedText() const { return QString(); }
QColor hoverColor() const { return hoverColor_; }
void setHoverColor(QColor &color) { hoverColor_ = color; }
bool hovering() const { return hovering_; }
void setHovering(const bool hover) { hovering_ = hover; };
protected:
void paintEvent(QPaintEvent *event) override;
signals:
void clicked(const QString &text);
protected:
QHBoxLayout *topLayout_;
Avatar *avatar_;
QColor hoverColor_;
//! Set if the item is currently being
//! hovered during tab completion (cycling).
bool hovering_;
};
class UserItem : public PopupItem
{
Q_OBJECT
public:
UserItem(QWidget *parent, const QString &user_id);
QString selectedText() const { return userId_; }
void updateItem(const QString &user_id);
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
void resolveAvatar(const QString &user_id);
QLabel *userName_;
QString userId_;
};
class RoomItem : public PopupItem
{
Q_OBJECT
public:
RoomItem(QWidget *parent, const RoomSearchResult &res);
QString selectedText() const { return roomId_; }
void updateItem(const RoomSearchResult &res);
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
QLabel *roomName_;
QString roomId_;
RoomSearchResult info_;
};

60
src/popups/ReplyPopup.cpp Normal file
View file

@ -0,0 +1,60 @@
#include <QPaintEvent>
#include <QLabel>
#include <QPainter>
#include <QStyleOption>
#include "../Config.h"
#include "../Utils.h"
#include "../ui/Avatar.h"
#include "../ui/DropShadow.h"
#include "ReplyPopup.h"
ReplyPopup::ReplyPopup(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_ShowWithoutActivating, true);
setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
layout_ = new QVBoxLayout(this);
layout_->setMargin(0);
layout_->setSpacing(0);
}
void
ReplyPopup::setReplyContent(const QString &user, const QString &msg, const QString &srcEvent)
{
QLayoutItem *child;
while ((child = layout_->takeAt(0)) != 0) {
delete child->widget();
delete child;
}
// Create a new widget if there isn't already one in that
// layout position.
// if (!item) {
auto userItem = new UserItem(this, user);
auto *text = new QLabel(this);
text->setText(msg);
auto *event = new QLabel(this);
event->setText(srcEvent);
connect(userItem, &UserItem::clicked, this, &ReplyPopup::userSelected);
layout_->addWidget(userItem);
layout_->addWidget(text);
layout_->addWidget(event);
// } else {
// Update the current widget with the new data.
// auto userWidget = qobject_cast<UserItem *>(item->widget());
// if (userWidget)
// userWidget->updateItem(users.at(i).user_id);
// }
adjustSize();
}
void
ReplyPopup::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

32
src/popups/ReplyPopup.h Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include <QHBoxLayout>
#include <QLabel>
#include <QPoint>
#include <QWidget>
#include "../AvatarProvider.h"
#include "../Cache.h"
#include "../ChatPage.h"
#include "PopupItem.h"
class ReplyPopup : public QWidget
{
Q_OBJECT
public:
explicit ReplyPopup(QWidget *parent = nullptr);
public slots:
void setReplyContent(const QString &user, const QString &msg, const QString &srcEvent);
protected:
void paintEvent(QPaintEvent *event) override;
signals:
void userSelected(const QString &user);
private:
QVBoxLayout *layout_;
};

View file

@ -0,0 +1,156 @@
#include <QPaintEvent>
#include <QPainter>
#include <QStyleOption>
#include "../Config.h"
#include "SuggestionsPopup.h"
#include "../Utils.h"
#include "../ui/Avatar.h"
#include "../ui/DropShadow.h"
SuggestionsPopup::SuggestionsPopup(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_ShowWithoutActivating, true);
setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
layout_ = new QVBoxLayout(this);
layout_->setMargin(0);
layout_->setSpacing(0);
}
void
SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms)
{
if (rooms.empty()) {
hide();
return;
}
const size_t layoutCount = layout_->count();
const size_t roomCount = rooms.size();
// Remove the extra widgets from the layout.
if (roomCount < layoutCount)
removeLayoutItemsAfter(roomCount - 1);
for (size_t i = 0; i < roomCount; ++i) {
auto item = layout_->itemAt(i);
// Create a new widget if there isn't already one in that
// layout position.
if (!item) {
auto room = new RoomItem(this, rooms.at(i));
connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected);
layout_->addWidget(room);
} else {
// Update the current widget with the new data.
auto room = qobject_cast<RoomItem *>(item->widget());
if (room)
room->updateItem(rooms.at(i));
}
}
resetSelection();
adjustSize();
resize(geometry().width(), 40 * rooms.size());
selectNextSuggestion();
}
void
SuggestionsPopup::addUsers(const QVector<SearchResult> &users)
{
if (users.isEmpty()) {
hide();
return;
}
const size_t layoutCount = layout_->count();
const size_t userCount = users.size();
// Remove the extra widgets from the layout.
if (userCount < layoutCount)
removeLayoutItemsAfter(userCount - 1);
for (size_t i = 0; i < userCount; ++i) {
auto item = layout_->itemAt(i);
// Create a new widget if there isn't already one in that
// layout position.
if (!item) {
auto user = new UserItem(this, users.at(i).user_id);
connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected);
layout_->addWidget(user);
} else {
// Update the current widget with the new data.
auto userWidget = qobject_cast<UserItem *>(item->widget());
if (userWidget)
userWidget->updateItem(users.at(i).user_id);
}
}
resetSelection();
adjustSize();
selectNextSuggestion();
}
void
SuggestionsPopup::hoverSelection()
{
resetHovering();
setHovering(selectedItem_);
update();
}
void
SuggestionsPopup::selectNextSuggestion()
{
selectedItem_++;
if (selectedItem_ >= layout_->count())
selectFirstItem();
hoverSelection();
}
void
SuggestionsPopup::selectPreviousSuggestion()
{
selectedItem_--;
if (selectedItem_ < 0)
selectLastItem();
hoverSelection();
}
void
SuggestionsPopup::resetHovering()
{
for (int i = 0; i < layout_->count(); ++i) {
const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget());
if (item)
item->setHovering(false);
}
}
void
SuggestionsPopup::setHovering(int pos)
{
const auto &item = layout_->itemAt(pos);
const auto &widget = qobject_cast<PopupItem *>(item->widget());
if (widget)
widget->setHovering(true);
}
void
SuggestionsPopup::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

View file

@ -5,82 +5,11 @@
#include <QPoint>
#include <QWidget>
#include "AvatarProvider.h"
#include "Cache.h"
#include "ChatPage.h"
#include "../AvatarProvider.h"
#include "../Cache.h"
#include "../ChatPage.h"
#include "PopupItem.h"
class Avatar;
struct SearchResult;
class PopupItem : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
Q_PROPERTY(bool hovering READ hovering WRITE setHovering)
public:
PopupItem(QWidget *parent);
QString selectedText() const { return QString(); }
QColor hoverColor() const { return hoverColor_; }
void setHoverColor(QColor &color) { hoverColor_ = color; }
bool hovering() const { return hovering_; }
void setHovering(const bool hover) { hovering_ = hover; };
protected:
void paintEvent(QPaintEvent *event) override;
signals:
void clicked(const QString &text);
protected:
QHBoxLayout *topLayout_;
Avatar *avatar_;
QColor hoverColor_;
//! Set if the item is currently being
//! hovered during tab completion (cycling).
bool hovering_;
};
class UserItem : public PopupItem
{
Q_OBJECT
public:
UserItem(QWidget *parent, const QString &user_id);
QString selectedText() const { return userId_; }
void updateItem(const QString &user_id);
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
void resolveAvatar(const QString &user_id);
QLabel *userName_;
QString userId_;
};
class RoomItem : public PopupItem
{
Q_OBJECT
public:
RoomItem(QWidget *parent, const RoomSearchResult &res);
QString selectedText() const { return roomId_; }
void updateItem(const RoomSearchResult &res);
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
QLabel *roomName_;
QString roomId_;
RoomSearchResult info_;
};
class SuggestionsPopup : public QWidget
{