From 9159b9ce22efa4972792b5400fd384457c349caa Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Tue, 11 Jun 2019 21:04:30 -0400 Subject: [PATCH] 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). --- CMakeLists.txt | 8 +- src/QuickSwitcher.cpp | 2 +- src/QuickSwitcher.h | 2 +- src/TextInputWidget.cpp | 76 ++++++--- src/TextInputWidget.h | 12 +- .../PopupItem.cpp} | 157 +----------------- src/popups/PopupItem.h | 83 +++++++++ src/popups/ReplyPopup.cpp | 60 +++++++ src/popups/ReplyPopup.h | 32 ++++ src/popups/SuggestionsPopup.cpp | 156 +++++++++++++++++ src/{ => popups}/SuggestionsPopup.h | 79 +-------- 11 files changed, 412 insertions(+), 255 deletions(-) rename src/{SuggestionsPopup.cpp => popups/PopupItem.cpp} (50%) create mode 100644 src/popups/PopupItem.h create mode 100644 src/popups/ReplyPopup.cpp create mode 100644 src/popups/ReplyPopup.h create mode 100644 src/popups/SuggestionsPopup.cpp rename src/{ => popups}/SuggestionsPopup.h (53%) diff --git a/CMakeLists.txt b/CMakeLists.txt index fda60b71..321234dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/QuickSwitcher.cpp b/src/QuickSwitcher.cpp index eb79a427..f8f6c001 100644 --- a/src/QuickSwitcher.cpp +++ b/src/QuickSwitcher.cpp @@ -23,7 +23,7 @@ #include #include "QuickSwitcher.h" -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" RoomSearchInput::RoomSearchInput(QWidget *parent) : TextField(parent) diff --git a/src/QuickSwitcher.h b/src/QuickSwitcher.h index 24b9adfa..05f7be07 100644 --- a/src/QuickSwitcher.h +++ b/src/QuickSwitcher.h @@ -22,7 +22,7 @@ #include #include -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" #include "ui/TextField.h" Q_DECLARE_METATYPE(std::vector) diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 340c6f7c..b4251a0e 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -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,29 +76,34 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) &FilteredTextEdit::uploadData); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); - connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { - popup_.hide(); - - auto cursor = textCursor(); - const int end = cursor.position(); - - cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); - cursor.setPosition(end, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - cursor.insertText(text); + 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(); + + 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, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectNextSuggestion); connect(this, &FilteredTextEdit::selectPreviousSuggestion, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectPreviousSuggestion); connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { - popup_.selectHoveredSuggestion(); + suggestionsPopup_.selectHoveredSuggestion(); }); previewDialog_.hide(); @@ -117,9 +123,9 @@ FilteredTextEdit::showResults(const QVector &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); diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index a12183d8..2936340b 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -28,7 +28,8 @@ #include #include -#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(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) diff --git a/src/SuggestionsPopup.cpp b/src/popups/PopupItem.cpp similarity index 50% rename from src/SuggestionsPopup.cpp rename to src/popups/PopupItem.cpp index 952d2ef3..b10cd32e 100644 --- a/src/SuggestionsPopup.cpp +++ b/src/popups/PopupItem.cpp @@ -2,11 +2,9 @@ #include #include -#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; @@ -146,151 +144,4 @@ RoomItem::mousePressEvent(QMouseEvent *event) emit clicked(selectedText()); 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 &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(item->widget()); - if (room) - room->updateItem(rooms.at(i)); - } - } - - resetSelection(); - adjustSize(); - - resize(geometry().width(), 40 * rooms.size()); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::addUsers(const QVector &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(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(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(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); -} +} \ 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 +#include +#include +#include + +#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 +#include +#include +#include + +#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(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 +#include +#include +#include + +#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 +#include +#include + +#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 &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(item->widget()); + if (room) + room->updateItem(rooms.at(i)); + } + } + + resetSelection(); + adjustSize(); + + resize(geometry().width(), 40 * rooms.size()); + + selectNextSuggestion(); +} + +void +SuggestionsPopup::addUsers(const QVector &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(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(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(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/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h similarity index 53% rename from src/SuggestionsPopup.h rename to src/popups/SuggestionsPopup.h index 72d6c7eb..4fbb97b3 100644 --- a/src/SuggestionsPopup.h +++ b/src/popups/SuggestionsPopup.h @@ -5,82 +5,11 @@ #include #include -#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 {