From 2dec9c46a7f62e1426ea8c3df9ecf994fc1c2383 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 22 Jul 2017 11:35:18 +0300 Subject: [PATCH] Add emoji autocomplete to history message field. --- .../chat_helpers/chat_helpers.style | 16 + .../chat_helpers/emoji_suggestions_widget.cpp | 504 ++++++++++++++++++ .../chat_helpers/emoji_suggestions_widget.h | 86 +++ .../SourceFiles/history/history_widget.cpp | 104 +++- Telegram/SourceFiles/history/history_widget.h | 5 + Telegram/SourceFiles/ui/twidget.h | 6 +- .../SourceFiles/ui/widgets/inner_dropdown.cpp | 66 ++- .../SourceFiles/ui/widgets/inner_dropdown.h | 13 +- Telegram/SourceFiles/ui/widgets/menu.h | 34 +- Telegram/SourceFiles/window/window.style | 7 + 10 files changed, 802 insertions(+), 39 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 74856e198..d1bf2d91f 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -222,3 +222,19 @@ gifsSearchCancel: contactsSearchCancel; gifsSearchCancelPosition: point(1px, 1px); gifsSearchIcon: boxFieldSearchIcon; gifsSearchIconPosition: point(6px, 7px); + +emojiSuggestionsDropdown: InnerDropdown(defaultInnerDropdown) { + scroll: ScrollArea(defaultSolidScroll) { + deltat: 0px; + deltab: 0px; + round: 1px; + width: 8px; + deltax: 3px; + } + scrollMargin: margins(0px, 5px, 0px, 5px); + scrollPadding: margins(0px, 3px, 0px, 3px); +} +emojiSuggestionsMenu: Menu(defaultMenu) { + itemPadding: margins(48px, 8px, 17px, 7px); + widthMax: 512px; +} diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index 02605991c..47321959c 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -20,3 +20,507 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "chat_helpers/emoji_suggestions_widget.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/shadow.h" +#include "platform/platform_specific.h" +#include "styles/style_chat_helpers.h" +#include "ui/widgets/inner_dropdown.h" + +namespace Ui { +namespace Emoji { +namespace { + +constexpr auto kRowLimit = 5; +constexpr auto kLargestReplacementLength = 128; + +} // namespace + +class SuggestionsWidget::Row { +public: + Row(gsl::not_null emoji, const QString &label, const QString &replacement); + Row(const Row &other) = delete; + Row &operator=(const Row &other) = delete; + Row(Row &&other) = default; + Row &operator=(Row &&other) = default; + ~Row(); + + gsl::not_null emoji() const { + return _emoji; + } + const QString &label() const { + return _label; + } + const QString &replacement() const { + return _replacement; + } + RippleAnimation *ripple() const { + return _ripple.get(); + } + void setRipple(std::unique_ptr ripple) { + _ripple = std::move(ripple); + } + void resetRipple() { + _ripple.reset(); + } + +private: + gsl::not_null _emoji; + QString _label; + QString _replacement; + std::unique_ptr _ripple; + +}; + +SuggestionsWidget::Row::Row(gsl::not_null emoji, const QString &label, const QString &replacement) +: _emoji(emoji) +, _label(label) +, _replacement(replacement) { +} + +SuggestionsWidget::Row::~Row() = default; + +SuggestionsWidget::SuggestionsWidget(QWidget *parent, const style::Menu &st) : TWidget(parent) +, _st(&st) +, _rowHeight(_st->itemPadding.top() + _st->itemFont->height + _st->itemPadding.bottom()) { + setMouseTracking(true); +} + +void SuggestionsWidget::showWithQuery(const QString &query) { + if (_query == query) { + return; + } + auto rows = std::vector(); + _query = query; + if (!_query.isEmpty()) { + rows.reserve(kRowLimit); + for (auto &item : GetSuggestions(_query)) { + if (auto emoji = Find(item.id)) { + rows.emplace_back(emoji, item.label, item.replacement); + if (rows.size() == kRowLimit) { + break; + } + } + } + } + if (rows.empty()) { + toggleAnimated.notify(false, true); + } + clearSelection(); + _rows = std::move(rows); + resizeToRows(); + update(); + if (!_rows.empty()) { + setSelected(0); + } + if (!_rows.empty()) { + toggleAnimated.notify(true, true); + } +} + +void SuggestionsWidget::resizeToRows() { + auto newWidth = 0; + for (auto &row : _rows) { + accumulate_max(newWidth, countWidth(row)); + } + newWidth = snap(newWidth, _st->widthMin, _st->widthMax); + auto newHeight = _st->skip + (_rows.size() * _rowHeight) + _st->skip; + resize(newWidth, newHeight); +} + +int SuggestionsWidget::countWidth(const Row &row) { + auto textw = _st->itemFont->width(row.label()); + return _st->itemPadding.left() + textw + _st->itemPadding.right(); +} + +void SuggestionsWidget::paintEvent(QPaintEvent *e) { + Painter p(this); + + auto ms = getms(); + auto clip = e->rect(); + + auto topskip = QRect(0, 0, width(), _st->skip); + auto bottomskip = QRect(0, height() - _st->skip, width(), _st->skip); + if (clip.intersects(topskip)) p.fillRect(clip.intersected(topskip), _st->itemBg); + if (clip.intersects(bottomskip)) p.fillRect(clip.intersected(bottomskip), _st->itemBg); + + auto top = _st->skip; + p.setFont(_st->itemFont); + auto from = floorclamp(clip.top() - top, _rowHeight, 0, _rows.size()); + auto to = ceilclamp(clip.top() + clip.height() - top, _rowHeight, 0, _rows.size()); + p.translate(0, top + from * _rowHeight); + for (auto i = from; i != to; ++i) { + auto &row = _rows[i]; + auto selected = (i == _selected || i == _pressed); + p.fillRect(0, 0, width(), _rowHeight, selected ? _st->itemBgOver : _st->itemBg); + if (auto ripple = row.ripple()) { + ripple->paint(p, 0, 0, width(), ms); + if (ripple->empty()) { + row.resetRipple(); + } + } + auto emoji = row.emoji(); + auto esize = Ui::Emoji::Size(Ui::Emoji::Index() + 1); + p.drawPixmapLeft((_st->itemPadding.left() - (esize / cIntRetinaFactor())) / 2, (_rowHeight - (esize / cIntRetinaFactor())) / 2, width(), App::emojiLarge(), QRect(emoji->x() * esize, emoji->y() * esize, esize, esize)); + p.setPen(selected ? _st->itemFgOver : _st->itemFg); + p.drawTextLeft(_st->itemPadding.left(), _st->itemPadding.top(), width(), row.label()); + p.translate(0, _rowHeight); + } +} + +void SuggestionsWidget::keyPressEvent(QKeyEvent *e) { + handleKeyEvent(e->key()); +} + +void SuggestionsWidget::handleKeyEvent(int key) { + if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Tab) { + if (_selected >= 0 && _selected < _rows.size()) { + triggered.notify(_rows[_selected].replacement(), true); + } + return; + } + if ((key != Qt::Key_Up && key != Qt::Key_Down) || _rows.size() < 1) { + return; + } + + auto delta = (key == Qt::Key_Down ? 1 : -1), start = _selected; + if (start < 0 || start >= _rows.size()) { + start = (delta > 0) ? (_rows.size() - 1) : 0; + } + auto newSelected = start + delta; + if (newSelected < 0) { + newSelected += _rows.size(); + } else if (newSelected >= _rows.size()) { + newSelected -= _rows.size(); + } + + _mouseSelection = false; + setSelected(newSelected); +} + +void SuggestionsWidget::setSelected(int selected) { + if (selected >= _rows.size()) { + selected = -1; + } + if (_selected != selected) { + updateSelectedItem(); + _selected = selected; + updateSelectedItem(); + } +} + +void SuggestionsWidget::setPressed(int pressed) { + if (pressed >= _rows.size()) { + pressed = -1; + } + if (_pressed != pressed) { + _pressed = pressed; + } +} + +void SuggestionsWidget::clearMouseSelection() { + if (_mouseSelection) { + clearSelection(); + } +} + +void SuggestionsWidget::clearSelection() { + _mouseSelection = false; + setSelected(-1); +} + +int SuggestionsWidget::itemTop(int index) { + if (index > _rows.size()) { + index = _rows.size(); + } + return _st->skip + (_rowHeight * index); +} + +void SuggestionsWidget::updateItem(int index) { + if (index >= 0 && index < _rows.size()) { + update(0, itemTop(index), width(), _rowHeight); + } +} + +void SuggestionsWidget::updateSelectedItem() { + updateItem(_selected); +} + +void SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) { + auto inner = rect().marginsRemoved(QMargins(0, _st->skip, 0, _st->skip)); + auto localPosition = e->pos(); + if (inner.contains(localPosition)) { + _mouseSelection = true; + updateSelection(e->globalPos()); + } else { + clearMouseSelection(); + } +} + +void SuggestionsWidget::updateSelection(QPoint globalPosition) { + if (!_mouseSelection) return; + + auto p = mapFromGlobal(globalPosition) - QPoint(0, _st->skip); + auto selected = (p.y() >= 0) ? (p.y() / _rowHeight) : -1; + setSelected((selected >= 0 && selected < _rows.size()) ? selected : -1); +} +void SuggestionsWidget::mousePressEvent(QMouseEvent *e) { + if (!_mouseSelection) { + return; + } + if (_selected >= 0 && _selected < _rows.size()) { + setPressed(_selected); + if (!_rows[_pressed].ripple()) { + auto mask = RippleAnimation::rectMask(QSize(width(), _rowHeight)); + _rows[_pressed].setRipple(std::make_unique(_st->ripple, std::move(mask), [this, selected = _pressed] { + updateItem(selected); + })); + } + _rows[_pressed].ripple()->add(mapFromGlobal(QCursor::pos()) - QPoint(0, itemTop(_pressed))); + } +} + +void SuggestionsWidget::mouseReleaseEvent(QMouseEvent *e) { + if (_pressed >= 0 && _pressed < _rows.size()) { + auto pressed = _pressed; + setPressed(-1); + if (_rows[pressed].ripple()) { + _rows[pressed].ripple()->lastStop(); + } + if (pressed == _selected) { + triggered.notify(_rows[_selected].replacement(), true); + } + } +} + +void SuggestionsWidget::enterEventHook(QEvent *e) { + auto mouse = QCursor::pos(); + if (!rect().marginsRemoved(QMargins(0, _st->skip, 0, _st->skip)).contains(mapFromGlobal(mouse))) { + clearMouseSelection(); + } + return TWidget::enterEventHook(e); +} + +void SuggestionsWidget::leaveEventHook(QEvent *e) { + clearMouseSelection(); + return TWidget::leaveEventHook(e); +} + +SuggestionsController::SuggestionsController(QWidget *parent, gsl::not_null field) : QObject(nullptr) +, _field(field) +, _container(parent, st::emojiSuggestionsDropdown) +, _suggestions(_container->setOwnedWidget(object_ptr(parent, st::emojiSuggestionsMenu))) { + _container->setAutoHiding(false); + + _field->installEventFilter(this); + connect(_field, &QTextEdit::textChanged, this, [this] { handleTextChange(); }); + connect(_field, &QTextEdit::cursorPositionChanged, this, [this] { handleCursorPositionChange(); }); + + subscribe(_suggestions->toggleAnimated, [this](bool visible) { suggestionsUpdated(visible); }); + subscribe(_suggestions->triggered, [this](QString replacement) { replaceCurrent(replacement); }); + updateForceHidden(); + + handleTextChange(); +} + +void SuggestionsController::handleTextChange() { + _ignoreCursorPositionChange = true; + InvokeQueued(this, [this] { _ignoreCursorPositionChange = false; }); + + auto query = getEmojiQuery(); + if (query.isEmpty() || _textChangeAfterKeyPress) { + _suggestions->showWithQuery(query); + } +} + +QString SuggestionsController::getEmojiQuery() { + auto cursor = _field->textCursor(); + auto position = _field->textCursor().position(); + if (cursor.anchor() != position) { + return QString(); + } + + auto findTextPart = [this, &position] { + auto document = _field->document(); + auto block = document->findBlock(position); + for (auto i = block.begin(); !i.atEnd(); ++i) { + auto fragment = i.fragment(); + if (!fragment.isValid()) continue; + + auto from = fragment.position(); + auto till = from + fragment.length(); + if (from >= position || till < position) { + continue; + } + if (fragment.charFormat().isImageFormat()) { + continue; + } + position -= from; + _queryStartPosition = from; + return fragment.text(); + } + return QString(); + }; + + auto text = findTextPart(); + if (text.isEmpty()) { + return QString(); + } + + auto isSuggestionChar = [](QChar ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '_') || (ch == '-') || (ch == '+'); + }; + auto isGoodCharBeforeSuggestion = [isSuggestionChar](QChar ch) { + return !isSuggestionChar(ch) || (ch == 0); + }; + t_assert(position > 0 && position <= text.size()); + for (auto i = position; i != 0;) { + auto ch = text[--i]; + if (ch == ':') { + auto beforeColon = (i < 1) ? QChar(0) : text[i - 1]; + if (isGoodCharBeforeSuggestion(beforeColon)) { + // At least one letter after colon. + if (position > i + 1) { + // Skip colon and the first letter. + _queryStartPosition += i + 2; + return text.mid(i, position - i); + } + } + return QString(); + } + if (position - i > kLargestReplacementLength) { + return QString(); + } + if (!isSuggestionChar(ch)) { + return QString(); + } + } + return QString(); +} + +void SuggestionsController::replaceCurrent(const QString &replacement) { + auto cursor = _field->textCursor(); + auto suggestion = getEmojiQuery(); + if (suggestion.isEmpty()) { + _suggestions->showWithQuery(QString()); + } else { + cursor.setPosition(cursor.position() - suggestion.size(), QTextCursor::KeepAnchor); + cursor.insertText(replacement + ' '); + } +} + +void SuggestionsController::handleCursorPositionChange() { + InvokeQueued(this, [this] { + if (_ignoreCursorPositionChange) { + return; + } + _suggestions->showWithQuery(QString()); + }); +} + +void SuggestionsController::suggestionsUpdated(bool visible) { + _shown = visible; + if (_shown) { + _container->resizeToContent(); + updateGeometry(); + if (!_forceHidden) { + _container->showAnimated(Ui::PanelAnimation::Origin::BottomLeft); + } + } else if (!_forceHidden) { + _container->hideAnimated(); + } +} + +void SuggestionsController::updateGeometry() { + auto cursor = _field->textCursor(); + cursor.setPosition(_queryStartPosition); + auto aroundRect = _field->cursorRect(cursor); + aroundRect.setTopLeft(_field->viewport()->mapToGlobal(aroundRect.topLeft())); + aroundRect.setTopLeft(_container->parentWidget()->mapFromGlobal(aroundRect.topLeft())); + auto boundingRect = _container->parentWidget()->rect(); + auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft; + auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft(); + point -= rtl() ? QPoint(_container->width() - st::emojiSuggestionsDropdown.padding.right(), _container->height()) : QPoint(st::emojiSuggestionsDropdown.padding.left(), _container->height()); + if (rtl()) { + if (point.x() < boundingRect.x()) { + point.setX(boundingRect.x()); + } + if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) { + point.setX(boundingRect.x() + boundingRect.width() - _container->width()); + } + } else { + if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) { + point.setX(boundingRect.x() + boundingRect.width() - _container->width()); + } + if (point.x() < boundingRect.x()) { + point.setX(boundingRect.x()); + } + } + if (point.y() < boundingRect.y()) { + point.setY(aroundRect.y() + aroundRect.height()); + origin = (origin == PanelAnimation::Origin::BottomRight) ? PanelAnimation::Origin::TopRight : PanelAnimation::Origin::TopLeft; + } + _container->move(point); +} + +void SuggestionsController::updateForceHidden() { + _forceHidden = !_field->isVisible(); + if (_forceHidden) { + _container->hideFast(); + } else if (_shown) { + _container->showFast(); + } +} + +bool SuggestionsController::eventFilter(QObject *object, QEvent *event) { + if (object == _field) { + auto type = event->type(); + switch (type) { + case QEvent::Move: + case QEvent::Resize: { + if (_shown) { + updateGeometry(); + } + } break; + + case QEvent::Show: + case QEvent::ShowToParent: + case QEvent::Hide: + case QEvent::HideToParent: { + updateForceHidden(); + } break; + + case QEvent::KeyPress: { + auto key = static_cast(event)->key(); + switch (key) { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Tab: + case Qt::Key_Up: + case Qt::Key_Down: + if (_shown && !_forceHidden) { + _suggestions->handleKeyEvent(key); + return true; + } + break; + + case Qt::Key_Escape: + if (_shown && !_forceHidden) { + _suggestions->showWithQuery(QString()); + return true; + } + break; + } + _textChangeAfterKeyPress = true; + InvokeQueued(this, [this] { _textChangeAfterKeyPress = false; }); + } break; + } + } + return QObject::eventFilter(object, event); +} + +void SuggestionsController::raise() { + _container->raise(); +} + +} // namespace Emoji +} // namespace Ui diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index 2e652aa1d..24500b4f5 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -20,3 +20,89 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #pragma once +#include "chat_helpers/emoji_suggestions.h" +#include "ui/effects/panel_animation.h" + +namespace Ui { + +class InnerDropdown; +class FlatTextarea; + +namespace Emoji { + +class SuggestionsWidget : public TWidget { +public: + SuggestionsWidget(QWidget *parent, const style::Menu &st); + + void showWithQuery(const QString &query); + void handleKeyEvent(int key); + + base::Observable toggleAnimated; + base::Observable triggered; + +protected: + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void enterEventHook(QEvent *e) override; + void leaveEventHook(QEvent *e) override; + +private: + class Row; + + void resizeToRows(); + int countWidth(const Row &row); + void setSelected(int selected); + void setPressed(int pressed); + void clearMouseSelection(); + void clearSelection(); + void updateSelectedItem(); + int itemTop(int index); + void updateItem(int index); + void updateSelection(QPoint globalPosition); + + gsl::not_null _st; + + QString _query; + std::vector _rows; + + int _rowHeight = 0; + bool _mouseSelection = false; + int _selected = -1; + int _pressed = -1; + +}; + +class SuggestionsController : public QObject, private base::Subscriber { +public: + SuggestionsController(QWidget *parent, gsl::not_null field); + + void raise(); + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + void handleCursorPositionChange(); + void handleTextChange(); + QString getEmojiQuery(); + void suggestionsUpdated(bool visible); + void updateGeometry(); + void updateForceHidden(); + void replaceCurrent(const QString &replacement); + + bool _shown = false; + bool _forceHidden = false; + int _queryStartPosition = 0; + bool _ignoreCursorPositionChange = false; + bool _textChangeAfterKeyPress = false; + QPointer _field; + object_ptr _container; + QPointer _suggestions; + +}; + +} // namespace Emoji +} // namespace Ui diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 1fd18ede6..e5184391e 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -73,6 +73,102 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "window/notifications_manager.h" #include "window/window_controller.h" #include "inline_bots/inline_results_widget.h" +#include "chat_helpers/emoji_suggestions_widget.h" + +// Smart pointer for QObject*, has move semantics, destroys object if it doesn't have a parent. +template +class test_ptr { +public: + test_ptr(std::nullptr_t) { + } + + // No default constructor, but constructors with at least + // one argument are simply make functions. + template + explicit test_ptr(Parent &&parent, Args&&... args) : _object(new Object(std::forward(parent), std::forward(args)...)) { + } + + test_ptr(const test_ptr &other) = delete; + test_ptr &operator=(const test_ptr &other) = delete; + test_ptr(test_ptr &&other) : _object(base::take(other._object)) { + } + test_ptr &operator=(test_ptr &&other) { + auto temp = std::move(other); + destroy(); + std::swap(_object, temp._object); + return *this; + } + + template ::value>> + test_ptr(test_ptr &&other) : _object(base::take(other._object)) { + } + + template ::value>> + test_ptr &operator=(test_ptr &&other) { + _object = base::take(other._object); + return *this; + } + + test_ptr &operator=(std::nullptr_t) { + _object = nullptr; + return *this; + } + + // So we can pass this pointer to methods like connect(). + Object *data() const { + return static_cast(_object); + } + operator Object*() const { + return data(); + } + + explicit operator bool() const { + return _object != nullptr; + } + + Object *operator->() const { + return data(); + } + Object &operator*() const { + return *data(); + } + + // Use that instead "= new Object(parent, ...)" + template + void create(Parent &&parent, Args&&... args) { + destroy(); + _object = new Object(std::forward(parent), std::forward(args)...); + } + void destroy() { + delete base::take(_object); + } + void destroyDelayed() { + if (_object) { + if (auto widget = base::up_cast(data())) { + widget->hide(); + } + base::take(_object)->deleteLater(); + } + } + + ~test_ptr() { + if (auto pointer = _object) { + if (!pointer->parent()) { + destroy(); + } + } + } + +private: + template + friend class test_ptr; + + QPointer _object; + +}; + +class TestClass; +test_ptr tmp = { nullptr }; namespace { @@ -620,6 +716,7 @@ HistoryWidget::HistoryWidget(QWidget *parent, gsl::not_null _field->setInsertFromMimeDataHook([this](const QMimeData *data) { return confirmSendingFiles(data, CompressConfirm::Auto, data->text()); }); + _emojiSuggestions.create(this, _field.data()); updateFieldSubmitSettings(); _field->hide(); @@ -930,6 +1027,7 @@ void HistoryWidget::orderWidgets() { if (_tabbedPanel) { _tabbedPanel->raise(); } + _emojiSuggestions->raise(); if (_tabbedSelectorToggleTooltip) { _tabbedSelectorToggleTooltip->raise(); } @@ -4299,9 +4397,9 @@ void HistoryWidget::onFieldFocused() { void HistoryWidget::onCheckFieldAutocomplete() { if (!_history || _a_show.animating()) return; - bool start = false; - bool isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot); - QString query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start); + auto start = false; + auto isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot); + auto query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start); if (!query.isEmpty()) { if (query.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtagsAndBots(); if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots(); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 472954c28..77f82901b 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -49,6 +49,9 @@ class SendButton; class FlatButton; class LinkButton; class RoundButton; +namespace Emoji { +class SuggestionsController; +} // namespace Emoji } // namespace Ui namespace Window { @@ -833,6 +836,8 @@ private: DragState _attachDrag = DragStateNone; object_ptr _attachDragDocument, _attachDragPhoto; + object_ptr _emojiSuggestions = { nullptr }; + bool _nonEmptySelection = false; TaskQueue _fileLoader; diff --git a/Telegram/SourceFiles/ui/twidget.h b/Telegram/SourceFiles/ui/twidget.h index b5b1d2aad..b3e6aec37 100644 --- a/Telegram/SourceFiles/ui/twidget.h +++ b/Telegram/SourceFiles/ui/twidget.h @@ -454,7 +454,7 @@ public: // So we can pass this pointer to methods like connect(). Object *data() const { - return static_cast(_object); + return static_cast(_object.data()); } operator Object*() const { return data(); @@ -504,14 +504,14 @@ private: template friend class object_ptr; - QObject *_object = nullptr; + QPointer _object; }; template inline object_ptr static_object_cast(object_ptr source) { auto result = object_ptr(nullptr); - result._object = static_cast(base::take(source._object)); + result._object = static_cast(base::take(source._object).data()); return std::move(result); } diff --git a/Telegram/SourceFiles/ui/widgets/inner_dropdown.cpp b/Telegram/SourceFiles/ui/widgets/inner_dropdown.cpp index a255b3822..c09f0d367 100644 --- a/Telegram/SourceFiles/ui/widgets/inner_dropdown.cpp +++ b/Telegram/SourceFiles/ui/widgets/inner_dropdown.cpp @@ -78,6 +78,8 @@ void InnerDropdown::resizeToContent() { } if (newWidth != width() || newHeight != height()) { resize(newWidth, newHeight); + update(); + finishAnimations(); } } @@ -129,30 +131,38 @@ void InnerDropdown::paintEvent(QPaintEvent *e) { } void InnerDropdown::enterEventHook(QEvent *e) { - showAnimated(_origin); + if (_autoHiding) { + showAnimated(_origin); + } return TWidget::enterEventHook(e); } void InnerDropdown::leaveEventHook(QEvent *e) { - auto ms = getms(); - if (_a_show.animating(ms) || _a_opacity.animating(ms)) { - hideAnimated(); - } else { - _hideTimer.start(300); + if (_autoHiding) { + auto ms = getms(); + if (_a_show.animating(ms) || _a_opacity.animating(ms)) { + hideAnimated(); + } else { + _hideTimer.start(300); + } } return TWidget::leaveEventHook(e); } void InnerDropdown::otherEnter() { - showAnimated(_origin); + if (_autoHiding) { + showAnimated(_origin); + } } void InnerDropdown::otherLeave() { - auto ms = getms(); - if (_a_show.animating(ms) || _a_opacity.animating(ms)) { - hideAnimated(); - } else { - _hideTimer.start(0); + if (_autoHiding) { + auto ms = getms(); + if (_a_show.animating(ms) || _a_opacity.animating(ms)) { + hideAnimated(); + } else { + _hideTimer.start(0); + } } } @@ -162,6 +172,10 @@ void InnerDropdown::setOrigin(PanelAnimation::Origin origin) { void InnerDropdown::showAnimated(PanelAnimation::Origin origin) { setOrigin(origin); + showAnimated(); +} + +void InnerDropdown::showAnimated() { _hideTimer.stop(); showStarted(); } @@ -177,17 +191,43 @@ void InnerDropdown::hideAnimated(HideOption option) { startOpacityAnimation(true); } +void InnerDropdown::finishAnimations() { + if (_a_show.animating()) { + _a_show.finish(); + showAnimationCallback(); + } + if (_showAnimation) { + _showAnimation.reset(); + showChildren(); + } + if (_a_opacity.animating()) { + _a_opacity.finish(); + opacityAnimationCallback(); + } +} + +void InnerDropdown::showFast() { + _hideTimer.stop(); + finishAnimations(); + if (isHidden()) { + showChildren(); + show(); + } + _hiding = false; +} + void InnerDropdown::hideFast() { if (isHidden()) return; _hideTimer.stop(); + finishAnimations(); _hiding = false; - _a_opacity.finish(); hideFinished(); } void InnerDropdown::hideFinished() { _a_show.finish(); + _showAnimation.reset(); _cache = QPixmap(); _ignoreShowEvents = false; if (!isHidden()) { diff --git a/Telegram/SourceFiles/ui/widgets/inner_dropdown.h b/Telegram/SourceFiles/ui/widgets/inner_dropdown.h index 6c70403f2..17d9ae8e8 100644 --- a/Telegram/SourceFiles/ui/widgets/inner_dropdown.h +++ b/Telegram/SourceFiles/ui/widgets/inner_dropdown.h @@ -45,12 +45,14 @@ public: return rect().marginsRemoved(_st.padding).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size())); } + void setAutoHiding(bool autoHiding) { + _autoHiding = autoHiding; + } void setMaxHeight(int newMaxHeight); void resizeToContent(); void otherEnter(); void otherLeave(); - void hideFast(); void setShowStartCallback(base::lambda callback) { _showStartCallback = std::move(callback); @@ -66,13 +68,17 @@ public: return _hiding && _a_opacity.animating(); } - void setOrigin(PanelAnimation::Origin origin); - void showAnimated(PanelAnimation::Origin origin); enum class HideOption { Default, IgnoreShow, }; + void showAnimated(); + void setOrigin(PanelAnimation::Origin origin); + void showAnimated(PanelAnimation::Origin origin); void hideAnimated(HideOption option = HideOption::Default); + void finishAnimations(); + void showFast(); + void hideFast(); protected: void resizeEvent(QResizeEvent *e) override; @@ -115,6 +121,7 @@ private: std::unique_ptr _showAnimation; Animation _a_show; + bool _autoHiding = true; bool _hiding = false; QPixmap _cache; Animation _a_opacity; diff --git a/Telegram/SourceFiles/ui/widgets/menu.h b/Telegram/SourceFiles/ui/widgets/menu.h index 99124ec0b..f0ec4c5fb 100644 --- a/Telegram/SourceFiles/ui/widgets/menu.h +++ b/Telegram/SourceFiles/ui/widgets/menu.h @@ -99,6 +99,23 @@ private slots: void actionChanged(); private: + struct ActionData { + ActionData() = default; + ActionData(const ActionData &other) = delete; + ActionData &operator=(const ActionData &other) = delete; + ActionData(ActionData &&other) = default; + ActionData &operator=(ActionData &&other) = default; + ~ActionData(); + + bool hasSubmenu = false; + QString text; + QString shortcut; + const style::icon *icon = nullptr; + const style::icon *iconOver = nullptr; + std::unique_ptr ripple; + std::unique_ptr toggle; + }; + void updateSelected(QPoint globalPosition); void init(); @@ -126,23 +143,6 @@ private: base::lambda _mousePressDelegate; base::lambda _mouseReleaseDelegate; - struct ActionData { - ActionData() = default; - ActionData(const ActionData &other) = delete; - ActionData &operator=(const ActionData &other) = delete; - ActionData(ActionData &&other) = default; - ActionData &operator=(ActionData &&other) = default; - ~ActionData(); - - bool hasSubmenu = false; - QString text; - QString shortcut; - const style::icon *icon = nullptr; - const style::icon *iconOver = nullptr; - std::unique_ptr ripple; - std::unique_ptr toggle; - }; - QMenu *_wappedMenu = nullptr; Actions _actions; std::vector _actionsData; diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index c3a553961..1b89cfc76 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -295,6 +295,13 @@ themeEditorDescriptionSkip: 10px; themeEditorNameFont: font(15px semibold); themeEditorCopyNameFont: font(fsize semibold); +windowEmojiSuggestionsPopup: PopupMenu(defaultPopupMenu) { + menu: Menu(defaultMenu) { + itemPadding: margins(48px, 8px, 17px, 7px); + widthMax: 512px; + } +} + // Mac specific macAccessoryWidth: 450.;