diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp
new file mode 100644
index 00000000..b10cd32e
--- /dev/null
+++ b/src/popups/PopupItem.cpp
@@ -0,0 +1,147 @@
+#include <QPaintEvent>
+#include <QPainter>
+#include <QStyleOption>
+
+#include "PopupItem.h"
+#include "../Utils.h"
+#include "../ui/Avatar.h"
+
+constexpr int PopupHMargin = 4;
+constexpr int PopupItemMargin = 3;
+
+PopupItem::PopupItem(QWidget *parent)
+ : QWidget(parent)
+ , avatar_{new Avatar(this)}
+ , hovering_{false}
+{
+ setMouseTracking(true);
+ setAttribute(Qt::WA_Hover);
+
+ topLayout_ = new QHBoxLayout(this);
+ topLayout_->setContentsMargins(
+ PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin);
+
+ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+}
+
+void
+PopupItem::paintEvent(QPaintEvent *)
+{
+ QStyleOption opt;
+ opt.init(this);
+ QPainter p(this);
+ style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+
+ if (underMouse() || hovering_)
+ p.fillRect(rect(), hoverColor_);
+}
+
+UserItem::UserItem(QWidget *parent, const QString &user_id)
+ : PopupItem(parent)
+ , userId_{user_id}
+{
+ auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_);
+
+ avatar_->setSize(conf::popup::avatar);
+ avatar_->setLetter(utils::firstChar(displayName));
+
+ // If it's a matrix id we use the second letter.
+ if (displayName.size() > 1 && displayName.at(0) == '@')
+ avatar_->setLetter(QChar(displayName.at(1)));
+
+ userName_ = new QLabel(displayName, this);
+
+ topLayout_->addWidget(avatar_);
+ topLayout_->addWidget(userName_, 1);
+
+ resolveAvatar(user_id);
+}
+
+void
+UserItem::updateItem(const QString &user_id)
+{
+ userId_ = user_id;
+
+ auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_);
+
+ // If it's a matrix id we use the second letter.
+ if (displayName.size() > 1 && displayName.at(0) == '@')
+ avatar_->setLetter(QChar(displayName.at(1)));
+ else
+ avatar_->setLetter(utils::firstChar(displayName));
+
+ userName_->setText(displayName);
+ resolveAvatar(user_id);
+}
+
+void
+UserItem::resolveAvatar(const QString &user_id)
+{
+ AvatarProvider::resolve(
+ ChatPage::instance()->currentRoom(), userId_, this, [this, user_id](const QImage &img) {
+ // The user on the widget when the avatar is resolved,
+ // might be different from the user that made the call.
+ if (user_id == userId_)
+ avatar_->setImage(img);
+ else
+ // We try to resolve the avatar again.
+ resolveAvatar(userId_);
+ });
+}
+
+void
+UserItem::mousePressEvent(QMouseEvent *event)
+{
+ if (event->buttons() != Qt::RightButton)
+ emit clicked(
+ Cache::displayName(ChatPage::instance()->currentRoom(), selectedText()));
+
+ QWidget::mousePressEvent(event);
+}
+
+RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res)
+ : PopupItem(parent)
+ , roomId_{QString::fromStdString(res.room_id)}
+{
+ auto name = QFontMetrics(QFont()).elidedText(
+ QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10);
+
+ avatar_->setSize(conf::popup::avatar + 6);
+ avatar_->setLetter(utils::firstChar(name));
+
+ roomName_ = new QLabel(name, this);
+ roomName_->setMargin(0);
+
+ topLayout_->addWidget(avatar_);
+ topLayout_->addWidget(roomName_, 1);
+
+ if (!res.img.isNull())
+ avatar_->setImage(res.img);
+}
+
+void
+RoomItem::updateItem(const RoomSearchResult &result)
+{
+ roomId_ = QString::fromStdString(std::move(result.room_id));
+
+ auto name =
+ QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)),
+ Qt::ElideRight,
+ parentWidget()->width() - 10);
+
+ roomName_->setText(name);
+
+ if (!result.img.isNull())
+ avatar_->setImage(result.img);
+ else
+ avatar_->setLetter(utils::firstChar(name));
+}
+
+void
+RoomItem::mousePressEvent(QMouseEvent *event)
+{
+ if (event->buttons() != Qt::RightButton)
+ emit clicked(selectedText());
+
+ QWidget::mousePressEvent(event);
+}
\ No newline at end of file
diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h
new file mode 100644
index 00000000..1fc54bf7
--- /dev/null
+++ b/src/popups/PopupItem.h
@@ -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_;
+};
\ No newline at end of file
diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp
new file mode 100644
index 00000000..a883739f
--- /dev/null
+++ b/src/popups/ReplyPopup.cpp
@@ -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);
+}
diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h
new file mode 100644
index 00000000..d8355e53
--- /dev/null
+++ b/src/popups/ReplyPopup.h
@@ -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_;
+
+};
diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp
new file mode 100644
index 00000000..6861a76a
--- /dev/null
+++ b/src/popups/SuggestionsPopup.cpp
@@ -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);
+}
diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h
new file mode 100644
index 00000000..4fbb97b3
--- /dev/null
+++ b/src/popups/SuggestionsPopup.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPoint>
+#include <QWidget>
+
+#include "../AvatarProvider.h"
+#include "../Cache.h"
+#include "../ChatPage.h"
+#include "PopupItem.h"
+
+
+class SuggestionsPopup : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit SuggestionsPopup(QWidget *parent = nullptr);
+
+ template<class Item>
+ void selectHoveredSuggestion()
+ {
+ const auto item = layout_->itemAt(selectedItem_);
+ if (!item)
+ return;
+
+ const auto &widget = qobject_cast<Item *>(item->widget());
+ emit itemSelected(
+ Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText()));
+
+ resetSelection();
+ }
+
+public slots:
+ void addUsers(const QVector<SearchResult> &users);
+ void addRooms(const std::vector<RoomSearchResult> &rooms);
+
+ //! Move to the next available suggestion item.
+ void selectNextSuggestion();
+ //! Move to the previous available suggestion item.
+ void selectPreviousSuggestion();
+ //! Remove hovering from all items.
+ void resetHovering();
+ //! Set hovering to the item in the given layout position.
+ void setHovering(int pos);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+
+signals:
+ void itemSelected(const QString &user);
+
+private:
+ void hoverSelection();
+ void resetSelection() { selectedItem_ = -1; }
+ void selectFirstItem() { selectedItem_ = 0; }
+ void selectLastItem() { selectedItem_ = layout_->count() - 1; }
+ void removeLayoutItemsAfter(size_t startingPos)
+ {
+ size_t posToRemove = layout_->count() - 1;
+
+ QLayoutItem *item;
+ while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != 0) {
+ delete item->widget();
+ delete item;
+
+ posToRemove = layout_->count() - 1;
+ }
+ }
+
+ QVBoxLayout *layout_;
+
+ //! Counter for tab completion (cycling).
+ int selectedItem_ = -1;
+};
|