diff --git a/CMakeLists.txt b/CMakeLists.txt index 3085cc76..c669262b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,6 +146,7 @@ set(SRC_FILES src/dialogs/JoinRoom.cc src/dialogs/LeaveRoom.cc src/dialogs/Logout.cc + src/dialogs/ReadReceipts.cc # Emoji src/emoji/Category.cc @@ -226,6 +227,7 @@ qt5_wrap_cpp(MOC_HEADERS include/dialogs/JoinRoom.h include/dialogs/LeaveRoom.h include/dialogs/Logout.h + include/dialogs/ReadReceipts.h # Emoji include/emoji/Category.h diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h index 906f2593..44bf1ad2 100644 --- a/include/AvatarProvider.h +++ b/include/AvatarProvider.h @@ -36,7 +36,7 @@ class AvatarProvider : public QObject public: static void init(QSharedPointer client); - static void resolve(const QString &userId, TimelineItem *item); + static void resolve(const QString &userId, std::function callback); static void setAvatarUrl(const QString &userId, const QUrl &url); static void clear(); @@ -48,5 +48,5 @@ private: using UserID = QString; static QMap avatars_; - static QMap> toBeResolved_; + static QMap>> toBeResolved_; }; diff --git a/include/Cache.h b/include/Cache.h index 1f6c59f0..ae58e418 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -18,11 +18,52 @@ #pragma once #include +#include #include #include class RoomState; +//! Used to uniquely identify a list of read receipts. +struct ReadReceiptKey +{ + std::string event_id; + std::string room_id; +}; + +inline void +to_json(json &j, const ReadReceiptKey &key) +{ + j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; +} + +inline void +from_json(const json &j, ReadReceiptKey &key) +{ + key.event_id = j.at("event_id").get(); + key.room_id = j.at("room_id").get(); +} + +//! Decribes a read receipt stored in cache. +struct ReadReceiptValue +{ + std::string user_id; + uint64_t ts; +}; + +inline void +to_json(json &j, const ReadReceiptValue &value) +{ + j = json{{"user_id", value.user_id}, {"ts", value.ts}}; +} + +inline void +from_json(const json &j, ReadReceiptValue &value) +{ + value.user_id = j.at("user_id").get(); + value.ts = j.at("ts").get(); +} + class Cache { public: @@ -48,6 +89,19 @@ public: bool isFormatValid(); void setCurrentFormat(); + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map>; + void updateReadReceipt(const std::string &room_id, const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + QByteArray image(const QString &url) const; void saveImage(const QString &url, const QByteArray &data); @@ -60,6 +114,7 @@ private: lmdb::dbi roomDb_; lmdb::dbi invitesDb_; lmdb::dbi imagesDb_; + lmdb::dbi readReceiptsDb_; bool isMounted_; diff --git a/include/ChatPage.h b/include/ChatPage.h index 24fc6a25..584424c0 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -42,6 +42,10 @@ class TypingDisplay; class UserInfoWidget; class UserSettings; +namespace dialogs { +class ReadReceipts; +} + constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; @@ -59,6 +63,9 @@ public: // Initialize all the components of the UI. void bootstrap(QString userid, QString homeserver, QString token); void showQuickSwitcher(); + void showReadReceipts(const QString &event_id); + + static ChatPage *instance() { return instance_; } signals: void contentLoaded(); @@ -84,6 +91,8 @@ private slots: void removeInvite(const QString &room_id); private: + static ChatPage *instance_; + using UserID = QString; using RoomStates = QMap; using Membership = mtx::events::StateEvent; @@ -150,6 +159,9 @@ private: QSharedPointer quickSwitcher_; QSharedPointer quickSwitcherModal_; + QSharedPointer receiptsDialog_; + QSharedPointer receiptsModal_; + // Matrix Client API provider. QSharedPointer client_; diff --git a/include/Config.h b/include/Config.h index 7d35094e..5492e5fb 100644 --- a/include/Config.h +++ b/include/Config.h @@ -15,6 +15,10 @@ static constexpr int emojiSize = 14; static constexpr int headerFontSize = 21; static constexpr int typingNotificationFontSize = 11; +namespace receipts { +static constexpr int font = 12; +} + namespace dialogs { static constexpr int labelSize = 15; } diff --git a/include/MainWindow.h b/include/MainWindow.h index 2d047b51..d7c5e41d 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -42,7 +42,7 @@ public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); - static MainWindow *instance(); + static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); protected: diff --git a/include/dialogs/ReadReceipts.h b/include/dialogs/ReadReceipts.h new file mode 100644 index 00000000..42a9e1b7 --- /dev/null +++ b/include/dialogs/ReadReceipts.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class Avatar; + +namespace dialogs { + +class ReceiptItem : public QWidget +{ + Q_OBJECT + +public: + ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp); + +private: + QString dateFormat(const QDateTime &then) const; + + QHBoxLayout *topLayout_; + QVBoxLayout *textLayout_; + + Avatar *avatar_; + + QLabel *userName_; + QLabel *timestamp_; +}; + +class ReadReceipts : public QFrame +{ + Q_OBJECT +public: + explicit ReadReceipts(QWidget *parent = nullptr); + +public slots: + void addUsers(const std::multimap &users); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QLabel *topLabel_; + + QListWidget *userList_; +}; +} // dialogs diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index f1498d1b..78fb95c9 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -87,6 +87,7 @@ public: protected: void paintEvent(QPaintEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; private: void init(); @@ -116,6 +117,9 @@ private: DescInfo descriptionMsg_; + QMenu *receiptsMenu_; + QAction *showReadReceipts_; + QHBoxLayout *topLayout_; QVBoxLayout *sideLayout_; // Avatar or Timestamp QVBoxLayout *mainLayout_; // Header & Message body @@ -156,7 +160,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, setupAvatarLayout(displayName); mainLayout_->addLayout(headerLayout_); - AvatarProvider::resolve(userid, this); + AvatarProvider::resolve(userid, [=](const QImage &img) { setUserAvatar(img); }); } else { setupSimpleLayout(); } @@ -199,7 +203,7 @@ TimelineItem::setupWidgetLayout(Widget *widget, mainLayout_->addLayout(headerLayout_); - AvatarProvider::resolve(sender, this); + AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); }); } else { setupSimpleLayout(); } diff --git a/include/ui/OverlayModal.h b/include/ui/OverlayModal.h index 167ba71a..5f6b6eee 100644 --- a/include/ui/OverlayModal.h +++ b/include/ui/OverlayModal.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -37,6 +38,7 @@ public: protected: void paintEvent(QPaintEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; private: int duration_; diff --git a/libs/matrix-structs b/libs/matrix-structs index acb73247..3555ec1c 160000 --- a/libs/matrix-structs +++ b/libs/matrix-structs @@ -1 +1 @@ -Subproject commit acb732474665343174209f0518c33a7ca0eb504a +Subproject commit 3555ec1cfc3865e0ae47c0fa53c9ea00f1e7cb36 diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 970a1d03..a78fb612 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -84,6 +84,7 @@ dialogs--Logout, dialogs--LeaveRoom, dialogs--CreateRoom, dialogs--InviteUsers, +dialogs--ReadReceipts, dialogs--JoinRoom { background-color: #383c4a; color: #caccd1; diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index 6bdb6aa6..ce86e212 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -86,6 +86,7 @@ dialogs--Logout, dialogs--LeaveRoom, dialogs--CreateRoom, dialogs--InviteUsers, +dialogs--ReadReceipts, dialogs--JoinRoom { background-color: white; color: #333; diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc index 334f72c3..b8ea9e20 100644 --- a/src/AvatarProvider.cc +++ b/src/AvatarProvider.cc @@ -18,12 +18,10 @@ #include "AvatarProvider.h" #include "MatrixClient.h" -#include "timeline/TimelineItem.h" - QSharedPointer AvatarProvider::client_; QMap AvatarProvider::avatars_; -QMap> AvatarProvider::toBeResolved_; +QMap>> AvatarProvider::toBeResolved_; void AvatarProvider::init(QSharedPointer client) @@ -37,11 +35,11 @@ void AvatarProvider::updateAvatar(const QString &uid, const QImage &img) { if (toBeResolved_.contains(uid)) { - auto items = toBeResolved_[uid]; + auto callbacks = toBeResolved_[uid]; // Update all the timeline items with the resolved avatar. - for (const auto item : items) - item->setUserAvatar(img); + for (const auto callback : callbacks) + callback(img); toBeResolved_.remove(uid); } @@ -53,7 +51,7 @@ AvatarProvider::updateAvatar(const QString &uid, const QImage &img) } void -AvatarProvider::resolve(const QString &userId, TimelineItem *item) +AvatarProvider::resolve(const QString &userId, std::function callback) { if (!avatars_.contains(userId)) return; @@ -61,7 +59,7 @@ AvatarProvider::resolve(const QString &userId, TimelineItem *item) auto img = avatars_[userId].img; if (!img.isNull()) { - item->setUserAvatar(img); + callback(img); return; } @@ -69,12 +67,12 @@ AvatarProvider::resolve(const QString &userId, TimelineItem *item) if (!toBeResolved_.contains(userId)) { client_->fetchUserAvatar(userId, avatars_[userId].url); - QList timelineItems; - timelineItems.push_back(item); + QList> items; + items.push_back(callback); - toBeResolved_.insert(userId, timelineItems); + toBeResolved_.insert(userId, items); } else { - toBeResolved_[userId].push_back(item); + toBeResolved_[userId].push_back(callback); } } diff --git a/src/Cache.cc b/src/Cache.cc index 06e45f13..e4e700b2 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -36,6 +36,7 @@ Cache::Cache(const QString &userId) , roomDb_{0} , invitesDb_{0} , imagesDb_{0} + , readReceiptsDb_{0} , isMounted_{false} , userId_{userId} {} @@ -89,11 +90,12 @@ Cache::setup() env_.open(statePath.toStdString().c_str()); } - auto txn = lmdb::txn::begin(env_); - stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE); - roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE); - imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE); + auto txn = lmdb::txn::begin(env_); + stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE); + roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE); + invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE); + imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, "read_receipts", MDB_CREATE); txn.commit(); @@ -449,3 +451,94 @@ Cache::setInvites(const std::map &invi unmount(); } } + +std::multimap +Cache::readReceipts(const QString &event_id, const QString &room_id) +{ + std::multimap receipts; + + ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; + nlohmann::json json_key = receipt_key; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto key = json_key.dump(); + + lmdb::val value; + + bool res = + lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value); + + txn.commit(); + + if (res) { + auto json_response = json::parse(std::string(value.data(), value.size())); + auto values = json_response.get>(); + + for (auto v : values) + receipts.emplace(v.ts, v.user_id); + } + + } catch (const lmdb::error &e) { + qCritical() << "readReceipts:" << e.what(); + } + + return receipts; +} + +using Receipts = std::map>; +void +Cache::updateReadReceipt(const std::string &room_id, const Receipts &receipts) +{ + for (auto receipt : receipts) { + const auto event_id = receipt.first; + auto event_receipts = receipt.second; + + ReadReceiptKey receipt_key{event_id, room_id}; + nlohmann::json json_key = receipt_key; + + try { + auto read_txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + const auto key = json_key.dump(); + + lmdb::val prev_value; + + bool exists = lmdb::dbi_get( + read_txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value); + + read_txn.commit(); + + std::vector saved_receipts; + + // If an entry for the event id already exists, we would + // merge the existing receipts with the new ones. + if (exists) { + auto json_value = + json::parse(std::string(prev_value.data(), prev_value.size())); + + // Retrieve the saved receipts. + saved_receipts = json_value.get>(); + } + + // Append the new ones. + for (auto event_receipt : event_receipts) + saved_receipts.push_back( + ReadReceiptValue{event_receipt.first, event_receipt.second}); + + // Save back the merged (or only the new) receipts. + nlohmann::json json_updated_value = saved_receipts; + std::string merged_receipts = json_updated_value.dump(); + + auto txn = lmdb::txn::begin(env_); + + lmdb::dbi_put(txn, + readReceiptsDb_, + lmdb::val(key.data(), key.size()), + lmdb::val(merged_receipts.data(), merged_receipts.size())); + + txn.commit(); + } catch (const lmdb::error &e) { + qCritical() << "updateReadReceipts:" << e.what(); + } + } +} diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 071fef71..3958e2c2 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -39,11 +39,14 @@ #include "UserInfoWidget.h" #include "UserSettingsPage.h" +#include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" constexpr int MAX_INITIAL_SYNC_FAILURES = 5; constexpr int SYNC_RETRY_TIMEOUT = 10000; +ChatPage *ChatPage::instance_ = nullptr; + ChatPage::ChatPage(QSharedPointer client, QSharedPointer userSettings, QWidget *parent) @@ -302,6 +305,8 @@ ChatPage::ChatPage(QSharedPointer client, }); AvatarProvider::init(client); + + instance_ = this; } void @@ -734,6 +739,12 @@ ChatPage::updateJoinedRooms(const std::mapsecond.ephemeral.typing); + if (it->second.ephemeral.receipts.size() > 0) + QtConcurrent::run(cache_.data(), + &Cache::updateReadReceipt, + it->first, + it->second.ephemeral.receipts); + const auto newStateEvents = it->second.state; const auto newTimelineEvents = it->second.timeline; @@ -809,4 +820,25 @@ ChatPage::generateMembershipDifference( return stateDiff; } +void +ChatPage::showReadReceipts(const QString &event_id) +{ + if (receiptsDialog_.isNull()) { + receiptsDialog_ = QSharedPointer( + new dialogs::ReadReceipts(this), + [=](dialogs::ReadReceipts *dialog) { dialog->deleteLater(); }); + } + + if (receiptsModal_.isNull()) { + receiptsModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), receiptsDialog_.data()), + [=](OverlayModal *modal) { modal->deleteLater(); }); + receiptsModal_->setDuration(0); + receiptsModal_->setColor(QColor(30, 30, 30, 170)); + } + + receiptsDialog_->addUsers(cache_->readReceipts(event_id, current_room_)); + receiptsModal_->fadeIn(); +} + ChatPage::~ChatPage() {} diff --git a/src/MainWindow.cc b/src/MainWindow.cc index 39f58dac..f2b7005b 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -281,10 +281,4 @@ MainWindow::hasActiveUser() settings.contains("auth/user_id"); } -MainWindow * -MainWindow::instance() -{ - return instance_; -} - MainWindow::~MainWindow() {} diff --git a/src/dialogs/ReadReceipts.cc b/src/dialogs/ReadReceipts.cc new file mode 100644 index 00000000..ae28969f --- /dev/null +++ b/src/dialogs/ReadReceipts.cc @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" + +#include "Avatar.h" +#include "AvatarProvider.h" +#include "dialogs/ReadReceipts.h" +#include "timeline/TimelineViewManager.h" + +using namespace dialogs; + +ReceiptItem::ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp) + : QWidget(parent) +{ + topLayout_ = new QHBoxLayout(this); + topLayout_->setMargin(0); + + textLayout_ = new QVBoxLayout; + textLayout_->setMargin(0); + textLayout_->setSpacing(5); + + QFont font; + font.setPixelSize(conf::receipts::font); + + auto displayName = TimelineViewManager::displayName(user_id); + + avatar_ = new Avatar(this); + avatar_->setSize(40); + avatar_->setLetter(QChar(displayName[0])); + + // 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); + userName_->setFont(font); + + timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this); + timestamp_->setFont(font); + + textLayout_->addWidget(userName_); + textLayout_->addWidget(timestamp_); + + topLayout_->addWidget(avatar_); + topLayout_->addLayout(textLayout_, 1); + + AvatarProvider::resolve(user_id, [=](const QImage &img) { avatar_->setImage(img); }); +} + +QString +ReceiptItem::dateFormat(const QDateTime &then) const +{ + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + + if (days == 0) + return QString("Today %1").arg(then.toString("HH:mm")); + else if (days < 2) + return QString("Yesterday %1").arg(then.toString("HH::mm")); + else if (days < 365) + return then.toString("dd/MM HH:mm"); + + return then.toString("dd/MM/yy"); +} + +ReadReceipts::ReadReceipts(QWidget *parent) + : QFrame(parent) +{ + setMaximumSize(400, 350); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(30); + layout->setMargin(20); + + userList_ = new QListWidget; + userList_->setFrameStyle(QFrame::NoFrame); + userList_->setSelectionMode(QAbstractItemView::NoSelection); + userList_->setAttribute(Qt::WA_MacShowFocusRect, 0); + userList_->setSpacing(5); + + QFont font; + font.setPixelSize(conf::headerFontSize); + + topLabel_ = new QLabel(tr("Read receipts"), this); + topLabel_->setAlignment(Qt::AlignCenter); + topLabel_->setFont(font); + + layout->addWidget(topLabel_); + layout->addWidget(userList_); +} + +void +ReadReceipts::addUsers(const std::multimap &receipts) +{ + // We want to remove any previous items that have been set. + userList_->clear(); + + for (auto receipt : receipts) { + auto user = + new ReceiptItem(this, QString::fromStdString(receipt.second), receipt.first); + auto item = new QListWidgetItem(userList_); + + item->setSizeHint(user->minimumSizeHint()); + item->setFlags(Qt::NoItemFlags); + item->setTextAlignment(Qt::AlignCenter); + + userList_->setItemWidget(item, user); + } +} + +void +ReadReceipts::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc index a42edbb7..202b331d 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc @@ -15,10 +15,13 @@ * along with this program. If not, see . */ +#include #include +#include #include #include "Avatar.h" +#include "ChatPage.h" #include "Config.h" #include "timeline/TimelineItem.h" @@ -39,6 +42,14 @@ TimelineItem::init() QFontMetrics fm(font_); + receiptsMenu_ = new QMenu(this); + showReadReceipts_ = new QAction("Read receipts", this); + receiptsMenu_->addAction(showReadReceipts_); + connect(showReadReceipts_, &QAction::triggered, this, [=]() { + if (!event_id_.isEmpty()) + ChatPage::instance()->showReadReceipts(event_id_); + }); + topLayout_ = new QHBoxLayout(this); sideLayout_ = new QVBoxLayout; mainLayout_ = new QVBoxLayout; @@ -88,7 +99,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, setupAvatarLayout(displayName); mainLayout_->addLayout(headerLayout_); - AvatarProvider::resolve(userid, this); + AvatarProvider::resolve(userid, [=](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -213,7 +224,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEventaddLayout(headerLayout_); - AvatarProvider::resolve(sender, this); + AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -252,7 +263,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent setupAvatarLayout(displayName); mainLayout_->addLayout(headerLayout_); - AvatarProvider::resolve(sender, this); + AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); }); } else { generateBody(emoteMsg); setupSimpleLayout(); @@ -297,7 +308,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent mainLayout_->addLayout(headerLayout_); - AvatarProvider::resolve(sender, this); + AvatarProvider::resolve(sender, [=](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -471,6 +482,13 @@ TimelineItem::descriptiveTime(const QDateTime &then) TimelineItem::~TimelineItem() {} +void +TimelineItem::contextMenuEvent(QContextMenuEvent *event) +{ + if (receiptsMenu_) + receiptsMenu_->exec(event->globalPos()); +} + void TimelineItem::paintEvent(QPaintEvent *) { diff --git a/src/ui/OverlayModal.cc b/src/ui/OverlayModal.cc index 4fb57175..290d28e5 100644 --- a/src/ui/OverlayModal.cc +++ b/src/ui/OverlayModal.cc @@ -47,6 +47,8 @@ OverlayModal::OverlayModal(QWidget *parent, QWidget *content) if (animation_->direction() == QAbstractAnimation::Forward) this->close(); }); + + content->setFocus(); } void @@ -72,3 +74,12 @@ OverlayModal::fadeOut() animation_->setDirection(QAbstractAnimation::Forward); animation_->start(); } + +void +OverlayModal::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) { + event->accept(); + fadeOut(); + } +}