| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "chat_helpers/emoji_suggestions_widget.h"
- #include "chat_helpers/emoji_keywords.h"
- #include "core/core_settings.h"
- #include "core/application.h"
- #include "emoji_suggestions_helper.h"
- #include "ui/effects/ripple_animation.h"
- #include "ui/widgets/shadow.h"
- #include "ui/widgets/inner_dropdown.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/emoji_config.h"
- #include "ui/ui_utility.h"
- #include "ui/cached_round_corners.h"
- #include "ui/round_rect.h"
- #include "platform/platform_specific.h"
- #include "core/application.h"
- #include "base/event_filter.h"
- #include "main/main_session.h"
- #include "data/data_session.h"
- #include "data/data_document.h"
- #include "data/stickers/data_custom_emoji.h"
- #include "data/stickers/data_stickers.h"
- #include "styles/style_chat_helpers.h"
- #include <QtWidgets/QApplication>
- #include <QtGui/QTextBlock>
- namespace Ui {
- namespace Emoji {
- namespace {
- constexpr auto kShowExactDelay = crl::time(300);
- constexpr auto kMaxNonScrolledEmoji = 7;
- } // namespace
- class SuggestionsWidget final : public Ui::RpWidget {
- public:
- SuggestionsWidget(
- QWidget *parent,
- const style::EmojiSuggestions &st,
- not_null<Main::Session*> session,
- bool suggestCustomEmoji,
- Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium);
- ~SuggestionsWidget();
- void showWithQuery(SuggestionsQuery query, bool force = false);
- void selectFirstResult();
- bool handleKeyEvent(int key);
- [[nodiscard]] rpl::producer<bool> toggleAnimated() const;
- struct Chosen {
- QString emoji;
- QString customData;
- };
- [[nodiscard]] rpl::producer<Chosen> triggered() const;
- private:
- struct Row {
- Row(not_null<EmojiPtr> emoji, const QString &replacement);
- Ui::Text::CustomEmoji *custom = nullptr;
- DocumentData *document = nullptr;
- not_null<EmojiPtr> emoji;
- QString replacement;
- };
- struct Custom {
- not_null<DocumentData*> document;
- not_null<EmojiPtr> emoji;
- QString replacement;
- };
- bool eventHook(QEvent *e) override;
- 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(QEnterEvent *e) override;
- void leaveEventHook(QEvent *e) override;
- void scrollByWheelEvent(not_null<QWheelEvent*> e);
- void paintFadings(QPainter &p) const;
- [[nodiscard]] std::vector<Row> getRowsByQuery(const QString &text) const;
- [[nodiscard]] base::flat_multi_map<int, Custom> lookupCustom(
- const std::vector<Row> &rows) const;
- [[nodiscard]] std::vector<Row> appendCustom(
- std::vector<Row> rows);
- [[nodiscard]] std::vector<Row> appendCustom(
- std::vector<Row> rows,
- const base::flat_multi_map<int, Custom> &custom);
- void resizeToRows();
- void setSelected(
- int selected,
- anim::type animated = anim::type::instant);
- void setPressed(int pressed);
- void clearMouseSelection();
- void clearSelection();
- void updateSelectedItem();
- void updateItem(int index);
- [[nodiscard]] QRect inner() const;
- [[nodiscard]] QPoint innerShift() const;
- [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const;
- void selectByMouse(QPoint globalPosition);
- bool triggerSelectedRow() const;
- void triggerRow(const Row &row) const;
- [[nodiscard]] int scrollCurrent() const;
- void scrollTo(int value, anim::type animated = anim::type::instant);
- void stopAnimations();
- [[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(
- not_null<DocumentData*> document);
- void customEmojiRepaint();
- const style::EmojiSuggestions &_st;
- const not_null<Main::Session*> _session;
- SuggestionsQuery _query;
- std::vector<Row> _rows;
- bool _suggestCustomEmoji = false;
- Fn<bool(not_null<DocumentData*>)> _allowCustomWithoutPremium;
- Ui::RoundRect _overRect;
- base::flat_map<
- not_null<DocumentData*>,
- std::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;
- bool _repaintScheduled = false;
- std::optional<QPoint> _lastMousePosition;
- bool _mouseSelection = false;
- int _selected = -1;
- int _pressed = -1;
- int _scrollValue = 0;
- Ui::Animations::Simple _scrollAnimation;
- Ui::Animations::Simple _selectedAnimation;
- int _scrollMax = 0;
- int _oneWidth = 0;
- QMargins _padding;
- QPoint _mousePressPosition;
- int _dragScrollStart = -1;
- rpl::event_stream<bool> _toggleAnimated;
- rpl::event_stream<Chosen> _triggered;
- };
- SuggestionsWidget::SuggestionsWidget(
- QWidget *parent,
- const style::EmojiSuggestions &st,
- not_null<Main::Session*> session,
- bool suggestCustomEmoji,
- Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium)
- : RpWidget(parent)
- , _st(st)
- , _session(session)
- , _suggestCustomEmoji(suggestCustomEmoji)
- , _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium))
- , _overRect(st::roundRadiusSmall, _st.overBg)
- , _oneWidth(st::emojiSuggestionSize)
- , _padding(st::emojiSuggestionsPadding) {
- resize(
- _oneWidth + _padding.left() + _padding.right(),
- _oneWidth + _padding.top() + _padding.bottom());
- setMouseTracking(true);
- }
- SuggestionsWidget::~SuggestionsWidget() = default;
- rpl::producer<bool> SuggestionsWidget::toggleAnimated() const {
- return _toggleAnimated.events();
- }
- auto SuggestionsWidget::triggered() const -> rpl::producer<Chosen> {
- return _triggered.events();
- }
- void SuggestionsWidget::showWithQuery(SuggestionsQuery query, bool force) {
- if (!force && (_query == query)) {
- return;
- }
- _query = query;
- auto rows = [&] {
- if (const auto emoji = std::get_if<EmojiPtr>(&query)) {
- return appendCustom(
- {},
- lookupCustom({ Row(*emoji, (*emoji)->text()) }));
- }
- return appendCustom(getRowsByQuery(v::get<QString>(query)));
- }();
- if (rows.empty()) {
- _toggleAnimated.fire(false);
- }
- clearSelection();
- setPressed(-1);
- _rows = std::move(rows);
- resizeToRows();
- update();
- Ui::PostponeCall(this, [=] {
- if (!_rows.empty()) {
- _toggleAnimated.fire(true);
- }
- });
- }
- void SuggestionsWidget::selectFirstResult() {
- if (!_rows.empty() && _selected < 0) {
- setSelected(0);
- }
- }
- auto SuggestionsWidget::appendCustom(std::vector<Row> rows)
- -> std::vector<Row> {
- const auto custom = lookupCustom(rows);
- return appendCustom(std::move(rows), custom);
- }
- auto SuggestionsWidget::lookupCustom(const std::vector<Row> &rows) const
- -> base::flat_multi_map<int, Custom> {
- if (rows.empty()
- || !_suggestCustomEmoji
- || !Core::App().settings().suggestAnimatedEmoji()) {
- return {};
- }
- auto custom = base::flat_multi_map<int, Custom>();
- const auto premium = _session->premium();
- const auto stickers = &_session->data().stickers();
- for (const auto setId : stickers->emojiSetsOrder()) {
- const auto i = stickers->sets().find(setId);
- if (i == end(stickers->sets())) {
- continue;
- }
- for (const auto &document : i->second->stickers) {
- if (!premium
- && document->isPremiumEmoji()
- && (!_allowCustomWithoutPremium
- || !_allowCustomWithoutPremium(document))) {
- // Skip the whole premium emoji set.
- break;
- }
- if (const auto sticker = document->sticker()) {
- if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {
- const auto original = emoji->original();
- const auto j = ranges::find_if(
- rows,
- [&](const Row &row) {
- return row.emoji->original() == original;
- });
- if (j != end(rows)) {
- custom.emplace(int(j - begin(rows)), Custom{
- .document = document,
- .emoji = emoji,
- .replacement = j->replacement,
- });
- }
- }
- }
- }
- }
- return custom;
- }
- auto SuggestionsWidget::appendCustom(
- std::vector<Row> rows,
- const base::flat_multi_map<int, Custom> &custom)
- -> std::vector<Row> {
- rows.reserve(rows.size() + custom.size());
- for (const auto &[position, one] : custom) {
- rows.push_back(Row(one.emoji, one.replacement));
- rows.back().document = one.document;
- rows.back().custom = resolveCustomEmoji(one.document);
- }
- return rows;
- }
- not_null<Ui::Text::CustomEmoji*> SuggestionsWidget::resolveCustomEmoji(
- not_null<DocumentData*> document) {
- const auto i = _customEmoji.find(document);
- if (i != end(_customEmoji)) {
- return i->second.get();
- }
- auto emoji = document->session().data().customEmojiManager().create(
- document,
- [=] { customEmojiRepaint(); },
- Data::CustomEmojiManager::SizeTag::Large);
- return _customEmoji.emplace(
- document,
- std::move(emoji)
- ).first->second.get();
- }
- void SuggestionsWidget::customEmojiRepaint() {
- if (_repaintScheduled) {
- return;
- }
- _repaintScheduled = true;
- update();
- }
- SuggestionsWidget::Row::Row(
- not_null<EmojiPtr> emoji,
- const QString &replacement)
- : emoji(emoji)
- , replacement(replacement) {
- }
- auto SuggestionsWidget::getRowsByQuery(const QString &text) const
- -> std::vector<Row> {
- if (text.isEmpty()) {
- return {};
- }
- const auto middle = (text[0] == ':');
- const auto real = middle ? text.mid(1) : text;
- const auto simple = [&] {
- if (!middle || text.size() > 2) {
- return false;
- }
- // Suggest :D and :-P only as exact matches.
- return ranges::none_of(text, [](QChar ch) { return ch.isLower(); });
- }();
- const auto exact = !middle || simple;
- const auto list = Core::App().emojiKeywords().queryMine(real, exact);
- using Entry = ChatHelpers::EmojiKeywords::Result;
- return ranges::views::all(
- list
- ) | ranges::views::transform([](const Entry &result) {
- return Row(result.emoji, result.replacement);
- }) | ranges::to_vector;
- }
- void SuggestionsWidget::resizeToRows() {
- const auto count = int(_rows.size());
- const auto scrolled = (count > kMaxNonScrolledEmoji);
- const auto fullWidth = count * _oneWidth;
- const auto newWidth = scrolled
- ? st::emojiSuggestionsScrolledWidth
- : fullWidth;
- _scrollMax = std::max(0, fullWidth - newWidth);
- if (_scrollValue > _scrollMax || scrollCurrent() > _scrollMax) {
- scrollTo(std::min(_scrollValue, _scrollMax));
- }
- resize(_padding.left() + newWidth + _padding.right(), height());
- update();
- }
- bool SuggestionsWidget::eventHook(QEvent *e) {
- if (e->type() == QEvent::Wheel) {
- selectByMouse(QCursor::pos());
- if (_selected >= 0 && _pressed < 0) {
- scrollByWheelEvent(static_cast<QWheelEvent*>(e));
- }
- }
- return RpWidget::eventHook(e);
- }
- void SuggestionsWidget::scrollByWheelEvent(not_null<QWheelEvent*> e) {
- const auto horizontal = (e->angleDelta().x() != 0);
- const auto vertical = (e->angleDelta().y() != 0);
- const auto current = scrollCurrent();
- const auto scroll = [&] {
- if (horizontal) {
- const auto delta = e->pixelDelta().x()
- ? e->pixelDelta().x()
- : e->angleDelta().x();
- return std::clamp(
- current - ((rtl() ? -1 : 1) * delta),
- 0,
- _scrollMax);
- } else if (vertical) {
- const auto delta = e->pixelDelta().y()
- ? e->pixelDelta().y()
- : e->angleDelta().y();
- return std::clamp(current - delta, 0, _scrollMax);
- }
- return current;
- }();
- if (current != scroll) {
- scrollTo(scroll);
- if (!_lastMousePosition) {
- _lastMousePosition = QCursor::pos();
- }
- selectByMouse(*_lastMousePosition);
- update();
- }
- }
- void SuggestionsWidget::paintEvent(QPaintEvent *e) {
- auto p = QPainter(this);
- _repaintScheduled = false;
- const auto clip = e->rect();
- p.fillRect(clip, _st.bg);
- const auto shift = innerShift();
- p.translate(-shift);
- const auto paint = clip.translated(shift);
- const auto from = std::max(paint.x(), 0) / _oneWidth;
- const auto till = std::min(
- (paint.x() + paint.width() + _oneWidth - 1) / _oneWidth,
- int(_rows.size()));
- const auto selected = (_pressed >= 0)
- ? _pressed
- : _selectedAnimation.value(_selected);
- if (selected > -1.) {
- _overRect.paint(
- p,
- QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth));
- }
- auto context = Ui::CustomEmoji::Context{
- .textColor = _st.textFg->c,
- .now = crl::now(),
- };
- for (auto i = from; i != till; ++i) {
- const auto &row = _rows[i];
- const auto emoji = row.emoji;
- const auto esize = Ui::Emoji::GetSizeLarge();
- const auto size = esize / style::DevicePixelRatio();
- const auto x = i * _oneWidth + (_oneWidth - size) / 2;
- const auto y = (_oneWidth - size) / 2;
- if (row.custom) {
- context.position = { x, y };
- row.custom->paint(p, context);
- } else {
- Ui::Emoji::Draw(p, emoji, esize, x, y);
- }
- }
- paintFadings(p);
- }
- void SuggestionsWidget::paintFadings(QPainter &p) const {
- const auto scroll = scrollCurrent();
- const auto o_left = std::clamp(
- scroll / float64(st::emojiSuggestionsFadeAfter),
- 0.,
- 1.);
- const auto shift = innerShift();
- if (o_left > 0.) {
- p.setOpacity(o_left);
- const auto rect = myrtlrect(
- shift.x(),
- 0,
- _st.fadeLeft.width(),
- height());
- _st.fadeLeft.fill(p, rect);
- p.setOpacity(1.);
- }
- const auto o_right = std::clamp(
- (_scrollMax - scroll) / float64(st::emojiSuggestionsFadeAfter),
- 0.,
- 1.);
- if (o_right > 0.) {
- p.setOpacity(o_right);
- const auto rect = myrtlrect(
- shift.x() + width() - _st.fadeRight.width(),
- 0,
- _st.fadeRight.width(),
- height());
- _st.fadeRight.fill(p, rect);
- p.setOpacity(1.);
- }
- }
- void SuggestionsWidget::keyPressEvent(QKeyEvent *e) {
- handleKeyEvent(e->key());
- }
- bool SuggestionsWidget::handleKeyEvent(int key) {
- if (key == Qt::Key_Enter || key == Qt::Key_Return) {
- return triggerSelectedRow();
- } else if (key == Qt::Key_Tab) {
- if (_selected < 0 || _selected >= _rows.size()) {
- setSelected(0);
- }
- return triggerSelectedRow();
- } else if (_rows.empty()
- || (key != Qt::Key_Up
- && key != Qt::Key_Down
- && key != Qt::Key_Left
- && key != Qt::Key_Right)) {
- return false;
- }
- const auto delta = (key == Qt::Key_Down || key == Qt::Key_Right)
- ? 1
- : -1;
- if (delta < 0 && _selected < 0) {
- return false;
- }
- auto start = _selected;
- if (start < 0 || start >= _rows.size()) {
- start = (delta > 0) ? (_rows.size() - 1) : 0;
- }
- auto newSelected = start + delta;
- if (newSelected < 0) {
- newSelected = -1;
- } else if (newSelected >= _rows.size()) {
- newSelected -= _rows.size();
- }
- _mouseSelection = false;
- _lastMousePosition = std::nullopt;
- setSelected(newSelected, anim::type::normal);
- return true;
- }
- void SuggestionsWidget::setSelected(int selected, anim::type animated) {
- if (selected >= _rows.size()) {
- selected = -1;
- }
- if (animated == anim::type::normal) {
- _selectedAnimation.start(
- [=] { update(); },
- _selected,
- selected,
- st::universalDuration,
- anim::sineInOut);
- if (_scrollMax > 0) {
- const auto selectedMax = int(_rows.size()) - 3;
- const auto selectedForScroll = std::min(
- std::max(selected, 1) - 1,
- selectedMax);
- scrollTo((_scrollMax * selectedForScroll) / selectedMax, animated);
- }
- } else if (_selectedAnimation.animating()) {
- _selectedAnimation.stop();
- update();
- }
- if (_selected != selected) {
- updateSelectedItem();
- _selected = selected;
- updateSelectedItem();
- }
- }
- int SuggestionsWidget::scrollCurrent() const {
- return _scrollAnimation.value(_scrollValue);
- }
- void SuggestionsWidget::scrollTo(int value, anim::type animated) {
- if (animated == anim::type::instant) {
- _scrollAnimation.stop();
- } else {
- _scrollAnimation.start(
- [=] { update(); },
- _scrollValue,
- value,
- st::universalDuration,
- anim::sineInOut);
- }
- _scrollValue = value;
- update();
- }
- void SuggestionsWidget::stopAnimations() {
- _scrollValue = _scrollAnimation.value(_scrollValue);
- _scrollAnimation.stop();
- }
- void SuggestionsWidget::setPressed(int pressed) {
- if (pressed >= _rows.size()) {
- pressed = -1;
- }
- if (_pressed != pressed) {
- _pressed = pressed;
- if (_pressed >= 0) {
- _mousePressPosition = QCursor::pos();
- }
- }
- }
- void SuggestionsWidget::clearMouseSelection() {
- if (_mouseSelection) {
- clearSelection();
- }
- }
- void SuggestionsWidget::clearSelection() {
- _mouseSelection = false;
- _lastMousePosition = std::nullopt;
- setSelected(-1);
- }
- void SuggestionsWidget::updateItem(int index) {
- if (index >= 0 && index < _rows.size()) {
- update(
- _padding.left() + index * _oneWidth - scrollCurrent(),
- _padding.top(),
- _oneWidth,
- _oneWidth);
- }
- }
- void SuggestionsWidget::updateSelectedItem() {
- updateItem(_selected);
- }
- QRect SuggestionsWidget::inner() const {
- return QRect(0, 0, _rows.size() * _oneWidth, _oneWidth);
- }
- QPoint SuggestionsWidget::innerShift() const {
- return QPoint(scrollCurrent() - _padding.left(), -_padding.top());
- }
- QPoint SuggestionsWidget::mapToInner(QPoint globalPosition) const {
- return mapFromGlobal(globalPosition) + innerShift();
- }
- void SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) {
- const auto globalPosition = e->globalPos();
- if (_dragScrollStart >= 0) {
- const auto delta = (_mousePressPosition.x() - globalPosition.x());
- const auto scroll = std::clamp(
- _dragScrollStart + (rtl() ? -1 : 1) * delta,
- 0,
- _scrollMax);
- if (scrollCurrent() != scroll) {
- scrollTo(scroll);
- update();
- }
- return;
- } else if ((_pressed >= 0)
- && (_scrollMax > 0)
- && ((_mousePressPosition - globalPosition).manhattanLength()
- >= QApplication::startDragDistance())) {
- _dragScrollStart = scrollCurrent();
- _mousePressPosition = globalPosition;
- scrollTo(_dragScrollStart);
- }
- if (inner().contains(mapToInner(globalPosition))) {
- if (!_lastMousePosition) {
- _lastMousePosition = globalPosition;
- return;
- } else if (!_mouseSelection
- && *_lastMousePosition == globalPosition) {
- return;
- }
- selectByMouse(globalPosition);
- } else {
- clearMouseSelection();
- }
- }
- void SuggestionsWidget::selectByMouse(QPoint globalPosition) {
- _mouseSelection = true;
- _lastMousePosition = globalPosition;
- const auto p = mapToInner(globalPosition);
- const auto selected = (p.x() >= 0) ? (p.x() / _oneWidth) : -1;
- setSelected((selected >= 0 && selected < _rows.size()) ? selected : -1);
- }
- void SuggestionsWidget::mousePressEvent(QMouseEvent *e) {
- selectByMouse(e->globalPos());
- if (_selected >= 0) {
- setPressed(_selected);
- }
- }
- void SuggestionsWidget::mouseReleaseEvent(QMouseEvent *e) {
- if (_pressed >= 0) {
- const auto pressed = _pressed;
- setPressed(-1);
- if (_dragScrollStart >= 0) {
- _dragScrollStart = -1;
- } else if (pressed == _selected) {
- triggerRow(_rows[_selected]);
- }
- }
- }
- bool SuggestionsWidget::triggerSelectedRow() const {
- if (_selected >= 0) {
- triggerRow(_rows[_selected]);
- return true;
- }
- return false;
- }
- void SuggestionsWidget::triggerRow(const Row &row) const {
- _triggered.fire({
- row.emoji->text(),
- (row.document
- ? Data::SerializeCustomEmojiId(row.document)
- : QString()),
- });
- }
- void SuggestionsWidget::enterEventHook(QEnterEvent *e) {
- if (!inner().contains(mapToInner(QCursor::pos()))) {
- clearMouseSelection();
- }
- return TWidget::enterEventHook(e);
- }
- void SuggestionsWidget::leaveEventHook(QEvent *e) {
- clearMouseSelection();
- return TWidget::leaveEventHook(e);
- }
- SuggestionsController::SuggestionsController(
- not_null<QWidget*> parent,
- not_null<QWidget*> outer,
- not_null<QTextEdit*> field,
- not_null<Main::Session*> session,
- const Options &options)
- : QObject(parent)
- , _st(options.st ? *options.st : st::defaultEmojiSuggestions)
- , _field(field)
- , _session(session)
- , _showExactTimer([=] { showWithQuery(getEmojiQuery()); })
- , _options(options) {
- _container = base::make_unique_q<InnerDropdown>(outer, _st.dropdown);
- _container->setAutoHiding(false);
- _suggestions = _container->setOwnedWidget(
- object_ptr<Ui::Emoji::SuggestionsWidget>(
- _container,
- _st,
- session,
- _options.suggestCustomEmoji,
- _options.allowCustomWithoutPremium));
- setReplaceCallback(nullptr);
- const auto fieldCallback = [=](not_null<QEvent*> event) {
- return (_container && fieldFilter(event))
- ? base::EventFilterResult::Cancel
- : base::EventFilterResult::Continue;
- };
- _fieldFilter.reset(base::install_event_filter(_field, fieldCallback));
- const auto outerCallback = [=](not_null<QEvent*> event) {
- return (_container && outerFilter(event))
- ? base::EventFilterResult::Cancel
- : base::EventFilterResult::Continue;
- };
- _outerFilter.reset(base::install_event_filter(outer, outerCallback));
- QObject::connect(
- _field,
- &QTextEdit::textChanged,
- _container,
- [=] { handleTextChange(); });
- QObject::connect(
- _field,
- &QTextEdit::cursorPositionChanged,
- _container,
- [=] { handleCursorPositionChange(); });
- _suggestions->toggleAnimated(
- ) | rpl::start_with_next([=](bool visible) {
- suggestionsUpdated(visible);
- }, _lifetime);
- _suggestions->triggered(
- ) | rpl::start_with_next([=](const SuggestionsWidget::Chosen &chosen) {
- replaceCurrent(chosen.emoji, chosen.customData);
- }, _lifetime);
- Core::App().emojiKeywords().refreshed(
- ) | rpl::start_with_next([=] {
- _keywordsRefreshed = true;
- if (!_showExactTimer.isActive()) {
- showWithQuery(_lastShownQuery);
- }
- }, _lifetime);
- updateForceHidden();
- _container->shownValue(
- ) | rpl::filter([=](bool shown) {
- return shown && !_shown;
- }) | rpl::start_with_next([=] {
- _container->hide();
- }, _container->lifetime());
- handleTextChange();
- }
- not_null<SuggestionsController*> SuggestionsController::Init(
- not_null<QWidget*> outer,
- not_null<Ui::InputField*> field,
- not_null<Main::Session*> session,
- const Options &options) {
- const auto result = Ui::CreateChild<SuggestionsController>(
- field.get(),
- outer,
- field->rawTextEdit(),
- session,
- options);
- result->setReplaceCallback([=](
- int from,
- int till,
- const QString &replacement,
- const QString &customEmojiData) {
- field->commitInstantReplacement(
- from,
- till,
- replacement,
- customEmojiData);
- });
- return result;
- }
- void SuggestionsController::setReplaceCallback(
- Fn<void(
- int from,
- int till,
- const QString &replacement,
- const QString &customEmojiData)> callback) {
- if (callback) {
- _replaceCallback = std::move(callback);
- } else {
- _replaceCallback = [=](
- int from,
- int till,
- const QString &replacement,
- const QString &customEmojiData) {
- auto cursor = _field->textCursor();
- cursor.setPosition(from);
- cursor.setPosition(till, QTextCursor::KeepAnchor);
- cursor.insertText(replacement);
- };
- }
- }
- void SuggestionsController::handleTextChange() {
- if (Core::App().settings().suggestEmoji()
- && _field->textCursor().position() > 0) {
- Core::App().emojiKeywords().refresh();
- }
- _ignoreCursorPositionChange = true;
- InvokeQueued(_container, [=] { _ignoreCursorPositionChange = false; });
- const auto query = getEmojiQuery();
- if (v::is<EmojiPtr>(query)) {
- showWithQuery(query);
- InvokeQueued(_container, [=] {
- if (_shown) {
- updateGeometry();
- }
- });
- return;
- }
- const auto text = v::get<QString>(query);
- if (text.isEmpty() || _textChangeAfterKeyPress) {
- const auto exact = !text.isEmpty() && (text[0] != ':');
- if (exact) {
- const auto hidden = _container->isHidden()
- || _container->isHiding();
- _showExactTimer.callOnce(hidden ? kShowExactDelay : 0);
- } else {
- showWithQuery(query);
- _suggestions->selectFirstResult();
- }
- }
- }
- void SuggestionsController::showWithQuery(SuggestionsQuery query) {
- _showExactTimer.cancel();
- const auto force = base::take(_keywordsRefreshed);
- _lastShownQuery = query;
- _suggestions->showWithQuery(_lastShownQuery, force);
- _container->resizeToContent();
- }
- SuggestionsQuery SuggestionsController::getEmojiQuery() {
- if (!Core::App().settings().suggestEmoji()) {
- return QString();
- }
- const auto cursor = _field->textCursor();
- if (cursor.hasSelection()) {
- return QString();
- }
- const auto modernLimit = Core::App().emojiKeywords().maxQueryLength();
- const auto legacyLimit = GetSuggestionMaxLength();
- const auto position = cursor.position();
- const auto findTextPart = [&]() -> SuggestionsQuery {
- auto previousFragmentStart = 0;
- auto previousFragmentName = QString();
- 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();
- const auto format = fragment.charFormat();
- if (format.objectType() == InputField::kCustomEmojiFormat) {
- previousFragmentName = QString();
- continue;
- } else if (format.isImageFormat()) {
- const auto imageName = format.toImageFormat().name();
- if (from >= position || till < position) {
- previousFragmentStart = from;
- previousFragmentName = imageName;
- continue;
- } else if (const auto emoji = Emoji::FromUrl(imageName)) {
- _queryStartPosition = position - 1;
- const auto start = (previousFragmentName == imageName)
- ? previousFragmentStart
- : from;
- _emojiQueryLength = (position - start);
- return emoji;
- } else {
- continue;
- }
- }
- if (from >= position || till < position) {
- previousFragmentName = QString();
- continue;
- }
- _queryStartPosition = from;
- _emojiQueryLength = 0;
- return fragment.text();
- }
- return QString();
- };
- const auto part = findTextPart();
- if (const auto emoji = std::get_if<EmojiPtr>(&part)) {
- return *emoji;
- }
- const auto text = v::get<QString>(part);
- if (text.isEmpty()) {
- return QString();
- }
- const auto length = position - _queryStartPosition;
- for (auto i = length; i != 0;) {
- if (text[--i] == ':') {
- const auto previous = (i > 0) ? text[i - 1] : QChar(0);
- if (i > 0 && (previous.isLetter() || previous.isDigit())) {
- return QString();
- } else if (i + 1 == length || text[i + 1].isSpace()) {
- return QString();
- }
- _queryStartPosition += i + 2;
- return text.mid(i, length - i);
- }
- if (length - i > legacyLimit && length - i > modernLimit) {
- return QString();
- }
- }
- // Exact query should be full input field value.
- const auto end = [&] {
- auto cursor = _field->textCursor();
- cursor.movePosition(QTextCursor::End);
- return cursor.position();
- }();
- if (!_options.suggestExactFirstWord
- || !length
- || text[0].isSpace()
- || (length > modernLimit)
- || (_queryStartPosition != 0)
- || (position != end)) {
- return QString();
- }
- return text;
- }
- void SuggestionsController::replaceCurrent(
- const QString &replacement,
- const QString &customEmojiData) {
- const auto cursor = _field->textCursor();
- const auto position = cursor.position();
- const auto suggestion = getEmojiQuery();
- if (v::is<EmojiPtr>(suggestion)) {
- const auto weak = Ui::MakeWeak(_container.get());
- const auto count = std::max(_emojiQueryLength, 1);
- for (auto i = 0; i != count; ++i) {
- const auto start = position - count + i;
- _replaceCallback(start, start + 1, replacement, customEmojiData);
- if (!weak) {
- return;
- }
- }
- } else if (v::get<QString>(suggestion).isEmpty()) {
- showWithQuery(QString());
- } else {
- const auto from = position - v::get<QString>(suggestion).size();
- _replaceCallback(from, position, replacement, customEmojiData);
- }
- }
- void SuggestionsController::handleCursorPositionChange() {
- InvokeQueued(_container, [=] {
- if (_ignoreCursorPositionChange) {
- return;
- }
- showWithQuery(QString());
- });
- }
- void SuggestionsController::suggestionsUpdated(bool visible) {
- _shown = visible;
- if (_shown) {
- _container->resizeToContent();
- updateGeometry();
- if (!_forceHidden) {
- if (_container->isHidden() || _container->isHiding()) {
- raise();
- }
- _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();
- const auto padding = _st.dropdown.padding;
- const auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2;
- point -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _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() || !_field->hasFocus();
- if (_forceHidden) {
- _container->hideFast();
- } else if (_shown) {
- _container->showFast();
- }
- }
- bool SuggestionsController::fieldFilter(not_null<QEvent*> event) {
- 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:
- case QEvent::FocusIn:
- case QEvent::FocusOut: {
- updateForceHidden();
- } break;
- case QEvent::KeyPress: {
- const auto key = static_cast<QKeyEvent*>(event.get())->key();
- switch (key) {
- case Qt::Key_Enter:
- case Qt::Key_Return:
- case Qt::Key_Tab:
- case Qt::Key_Up:
- case Qt::Key_Down:
- case Qt::Key_Left:
- case Qt::Key_Right:
- if (_shown && !_forceHidden) {
- return _suggestions->handleKeyEvent(key);
- }
- break;
- case Qt::Key_Escape:
- if (_shown && !_forceHidden) {
- showWithQuery(QString());
- return true;
- }
- break;
- }
- _textChangeAfterKeyPress = true;
- InvokeQueued(_container, [=] { _textChangeAfterKeyPress = false; });
- } break;
- }
- return false;
- }
- bool SuggestionsController::outerFilter(not_null<QEvent*> event) {
- auto type = event->type();
- switch (type) {
- case QEvent::Move:
- case QEvent::Resize: {
- // updateGeometry uses not only container geometry, but also
- // container children geometries that will be updated later.
- InvokeQueued(_container, [=] {
- if (_shown) {
- updateGeometry();
- }
- });
- } break;
- }
- return false;
- }
- void SuggestionsController::raise() {
- _container->raise();
- }
- } // namespace Emoji
- } // namespace Ui
|