emoji_suggestions_widget.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "chat_helpers/emoji_suggestions_widget.h"
  8. #include "chat_helpers/emoji_keywords.h"
  9. #include "core/core_settings.h"
  10. #include "core/application.h"
  11. #include "emoji_suggestions_helper.h"
  12. #include "ui/effects/ripple_animation.h"
  13. #include "ui/widgets/shadow.h"
  14. #include "ui/widgets/inner_dropdown.h"
  15. #include "ui/widgets/fields/input_field.h"
  16. #include "ui/emoji_config.h"
  17. #include "ui/ui_utility.h"
  18. #include "ui/cached_round_corners.h"
  19. #include "ui/round_rect.h"
  20. #include "platform/platform_specific.h"
  21. #include "core/application.h"
  22. #include "base/event_filter.h"
  23. #include "main/main_session.h"
  24. #include "data/data_session.h"
  25. #include "data/data_document.h"
  26. #include "data/stickers/data_custom_emoji.h"
  27. #include "data/stickers/data_stickers.h"
  28. #include "styles/style_chat_helpers.h"
  29. #include <QtWidgets/QApplication>
  30. #include <QtGui/QTextBlock>
  31. namespace Ui {
  32. namespace Emoji {
  33. namespace {
  34. constexpr auto kShowExactDelay = crl::time(300);
  35. constexpr auto kMaxNonScrolledEmoji = 7;
  36. } // namespace
  37. class SuggestionsWidget final : public Ui::RpWidget {
  38. public:
  39. SuggestionsWidget(
  40. QWidget *parent,
  41. const style::EmojiSuggestions &st,
  42. not_null<Main::Session*> session,
  43. bool suggestCustomEmoji,
  44. Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium);
  45. ~SuggestionsWidget();
  46. void showWithQuery(SuggestionsQuery query, bool force = false);
  47. void selectFirstResult();
  48. bool handleKeyEvent(int key);
  49. [[nodiscard]] rpl::producer<bool> toggleAnimated() const;
  50. struct Chosen {
  51. QString emoji;
  52. QString customData;
  53. };
  54. [[nodiscard]] rpl::producer<Chosen> triggered() const;
  55. private:
  56. struct Row {
  57. Row(not_null<EmojiPtr> emoji, const QString &replacement);
  58. Ui::Text::CustomEmoji *custom = nullptr;
  59. DocumentData *document = nullptr;
  60. not_null<EmojiPtr> emoji;
  61. QString replacement;
  62. };
  63. struct Custom {
  64. not_null<DocumentData*> document;
  65. not_null<EmojiPtr> emoji;
  66. QString replacement;
  67. };
  68. bool eventHook(QEvent *e) override;
  69. void paintEvent(QPaintEvent *e) override;
  70. void keyPressEvent(QKeyEvent *e) override;
  71. void mouseMoveEvent(QMouseEvent *e) override;
  72. void mousePressEvent(QMouseEvent *e) override;
  73. void mouseReleaseEvent(QMouseEvent *e) override;
  74. void enterEventHook(QEnterEvent *e) override;
  75. void leaveEventHook(QEvent *e) override;
  76. void scrollByWheelEvent(not_null<QWheelEvent*> e);
  77. void paintFadings(QPainter &p) const;
  78. [[nodiscard]] std::vector<Row> getRowsByQuery(const QString &text) const;
  79. [[nodiscard]] base::flat_multi_map<int, Custom> lookupCustom(
  80. const std::vector<Row> &rows) const;
  81. [[nodiscard]] std::vector<Row> appendCustom(
  82. std::vector<Row> rows);
  83. [[nodiscard]] std::vector<Row> appendCustom(
  84. std::vector<Row> rows,
  85. const base::flat_multi_map<int, Custom> &custom);
  86. void resizeToRows();
  87. void setSelected(
  88. int selected,
  89. anim::type animated = anim::type::instant);
  90. void setPressed(int pressed);
  91. void clearMouseSelection();
  92. void clearSelection();
  93. void updateSelectedItem();
  94. void updateItem(int index);
  95. [[nodiscard]] QRect inner() const;
  96. [[nodiscard]] QPoint innerShift() const;
  97. [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const;
  98. void selectByMouse(QPoint globalPosition);
  99. bool triggerSelectedRow() const;
  100. void triggerRow(const Row &row) const;
  101. [[nodiscard]] int scrollCurrent() const;
  102. void scrollTo(int value, anim::type animated = anim::type::instant);
  103. void stopAnimations();
  104. [[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(
  105. not_null<DocumentData*> document);
  106. void customEmojiRepaint();
  107. const style::EmojiSuggestions &_st;
  108. const not_null<Main::Session*> _session;
  109. SuggestionsQuery _query;
  110. std::vector<Row> _rows;
  111. bool _suggestCustomEmoji = false;
  112. Fn<bool(not_null<DocumentData*>)> _allowCustomWithoutPremium;
  113. Ui::RoundRect _overRect;
  114. base::flat_map<
  115. not_null<DocumentData*>,
  116. std::unique_ptr<Ui::Text::CustomEmoji>> _customEmoji;
  117. bool _repaintScheduled = false;
  118. std::optional<QPoint> _lastMousePosition;
  119. bool _mouseSelection = false;
  120. int _selected = -1;
  121. int _pressed = -1;
  122. int _scrollValue = 0;
  123. Ui::Animations::Simple _scrollAnimation;
  124. Ui::Animations::Simple _selectedAnimation;
  125. int _scrollMax = 0;
  126. int _oneWidth = 0;
  127. QMargins _padding;
  128. QPoint _mousePressPosition;
  129. int _dragScrollStart = -1;
  130. rpl::event_stream<bool> _toggleAnimated;
  131. rpl::event_stream<Chosen> _triggered;
  132. };
  133. SuggestionsWidget::SuggestionsWidget(
  134. QWidget *parent,
  135. const style::EmojiSuggestions &st,
  136. not_null<Main::Session*> session,
  137. bool suggestCustomEmoji,
  138. Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium)
  139. : RpWidget(parent)
  140. , _st(st)
  141. , _session(session)
  142. , _suggestCustomEmoji(suggestCustomEmoji)
  143. , _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium))
  144. , _overRect(st::roundRadiusSmall, _st.overBg)
  145. , _oneWidth(st::emojiSuggestionSize)
  146. , _padding(st::emojiSuggestionsPadding) {
  147. resize(
  148. _oneWidth + _padding.left() + _padding.right(),
  149. _oneWidth + _padding.top() + _padding.bottom());
  150. setMouseTracking(true);
  151. }
  152. SuggestionsWidget::~SuggestionsWidget() = default;
  153. rpl::producer<bool> SuggestionsWidget::toggleAnimated() const {
  154. return _toggleAnimated.events();
  155. }
  156. auto SuggestionsWidget::triggered() const -> rpl::producer<Chosen> {
  157. return _triggered.events();
  158. }
  159. void SuggestionsWidget::showWithQuery(SuggestionsQuery query, bool force) {
  160. if (!force && (_query == query)) {
  161. return;
  162. }
  163. _query = query;
  164. auto rows = [&] {
  165. if (const auto emoji = std::get_if<EmojiPtr>(&query)) {
  166. return appendCustom(
  167. {},
  168. lookupCustom({ Row(*emoji, (*emoji)->text()) }));
  169. }
  170. return appendCustom(getRowsByQuery(v::get<QString>(query)));
  171. }();
  172. if (rows.empty()) {
  173. _toggleAnimated.fire(false);
  174. }
  175. clearSelection();
  176. setPressed(-1);
  177. _rows = std::move(rows);
  178. resizeToRows();
  179. update();
  180. Ui::PostponeCall(this, [=] {
  181. if (!_rows.empty()) {
  182. _toggleAnimated.fire(true);
  183. }
  184. });
  185. }
  186. void SuggestionsWidget::selectFirstResult() {
  187. if (!_rows.empty() && _selected < 0) {
  188. setSelected(0);
  189. }
  190. }
  191. auto SuggestionsWidget::appendCustom(std::vector<Row> rows)
  192. -> std::vector<Row> {
  193. const auto custom = lookupCustom(rows);
  194. return appendCustom(std::move(rows), custom);
  195. }
  196. auto SuggestionsWidget::lookupCustom(const std::vector<Row> &rows) const
  197. -> base::flat_multi_map<int, Custom> {
  198. if (rows.empty()
  199. || !_suggestCustomEmoji
  200. || !Core::App().settings().suggestAnimatedEmoji()) {
  201. return {};
  202. }
  203. auto custom = base::flat_multi_map<int, Custom>();
  204. const auto premium = _session->premium();
  205. const auto stickers = &_session->data().stickers();
  206. for (const auto setId : stickers->emojiSetsOrder()) {
  207. const auto i = stickers->sets().find(setId);
  208. if (i == end(stickers->sets())) {
  209. continue;
  210. }
  211. for (const auto &document : i->second->stickers) {
  212. if (!premium
  213. && document->isPremiumEmoji()
  214. && (!_allowCustomWithoutPremium
  215. || !_allowCustomWithoutPremium(document))) {
  216. // Skip the whole premium emoji set.
  217. break;
  218. }
  219. if (const auto sticker = document->sticker()) {
  220. if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {
  221. const auto original = emoji->original();
  222. const auto j = ranges::find_if(
  223. rows,
  224. [&](const Row &row) {
  225. return row.emoji->original() == original;
  226. });
  227. if (j != end(rows)) {
  228. custom.emplace(int(j - begin(rows)), Custom{
  229. .document = document,
  230. .emoji = emoji,
  231. .replacement = j->replacement,
  232. });
  233. }
  234. }
  235. }
  236. }
  237. }
  238. return custom;
  239. }
  240. auto SuggestionsWidget::appendCustom(
  241. std::vector<Row> rows,
  242. const base::flat_multi_map<int, Custom> &custom)
  243. -> std::vector<Row> {
  244. rows.reserve(rows.size() + custom.size());
  245. for (const auto &[position, one] : custom) {
  246. rows.push_back(Row(one.emoji, one.replacement));
  247. rows.back().document = one.document;
  248. rows.back().custom = resolveCustomEmoji(one.document);
  249. }
  250. return rows;
  251. }
  252. not_null<Ui::Text::CustomEmoji*> SuggestionsWidget::resolveCustomEmoji(
  253. not_null<DocumentData*> document) {
  254. const auto i = _customEmoji.find(document);
  255. if (i != end(_customEmoji)) {
  256. return i->second.get();
  257. }
  258. auto emoji = document->session().data().customEmojiManager().create(
  259. document,
  260. [=] { customEmojiRepaint(); },
  261. Data::CustomEmojiManager::SizeTag::Large);
  262. return _customEmoji.emplace(
  263. document,
  264. std::move(emoji)
  265. ).first->second.get();
  266. }
  267. void SuggestionsWidget::customEmojiRepaint() {
  268. if (_repaintScheduled) {
  269. return;
  270. }
  271. _repaintScheduled = true;
  272. update();
  273. }
  274. SuggestionsWidget::Row::Row(
  275. not_null<EmojiPtr> emoji,
  276. const QString &replacement)
  277. : emoji(emoji)
  278. , replacement(replacement) {
  279. }
  280. auto SuggestionsWidget::getRowsByQuery(const QString &text) const
  281. -> std::vector<Row> {
  282. if (text.isEmpty()) {
  283. return {};
  284. }
  285. const auto middle = (text[0] == ':');
  286. const auto real = middle ? text.mid(1) : text;
  287. const auto simple = [&] {
  288. if (!middle || text.size() > 2) {
  289. return false;
  290. }
  291. // Suggest :D and :-P only as exact matches.
  292. return ranges::none_of(text, [](QChar ch) { return ch.isLower(); });
  293. }();
  294. const auto exact = !middle || simple;
  295. const auto list = Core::App().emojiKeywords().queryMine(real, exact);
  296. using Entry = ChatHelpers::EmojiKeywords::Result;
  297. return ranges::views::all(
  298. list
  299. ) | ranges::views::transform([](const Entry &result) {
  300. return Row(result.emoji, result.replacement);
  301. }) | ranges::to_vector;
  302. }
  303. void SuggestionsWidget::resizeToRows() {
  304. const auto count = int(_rows.size());
  305. const auto scrolled = (count > kMaxNonScrolledEmoji);
  306. const auto fullWidth = count * _oneWidth;
  307. const auto newWidth = scrolled
  308. ? st::emojiSuggestionsScrolledWidth
  309. : fullWidth;
  310. _scrollMax = std::max(0, fullWidth - newWidth);
  311. if (_scrollValue > _scrollMax || scrollCurrent() > _scrollMax) {
  312. scrollTo(std::min(_scrollValue, _scrollMax));
  313. }
  314. resize(_padding.left() + newWidth + _padding.right(), height());
  315. update();
  316. }
  317. bool SuggestionsWidget::eventHook(QEvent *e) {
  318. if (e->type() == QEvent::Wheel) {
  319. selectByMouse(QCursor::pos());
  320. if (_selected >= 0 && _pressed < 0) {
  321. scrollByWheelEvent(static_cast<QWheelEvent*>(e));
  322. }
  323. }
  324. return RpWidget::eventHook(e);
  325. }
  326. void SuggestionsWidget::scrollByWheelEvent(not_null<QWheelEvent*> e) {
  327. const auto horizontal = (e->angleDelta().x() != 0);
  328. const auto vertical = (e->angleDelta().y() != 0);
  329. const auto current = scrollCurrent();
  330. const auto scroll = [&] {
  331. if (horizontal) {
  332. const auto delta = e->pixelDelta().x()
  333. ? e->pixelDelta().x()
  334. : e->angleDelta().x();
  335. return std::clamp(
  336. current - ((rtl() ? -1 : 1) * delta),
  337. 0,
  338. _scrollMax);
  339. } else if (vertical) {
  340. const auto delta = e->pixelDelta().y()
  341. ? e->pixelDelta().y()
  342. : e->angleDelta().y();
  343. return std::clamp(current - delta, 0, _scrollMax);
  344. }
  345. return current;
  346. }();
  347. if (current != scroll) {
  348. scrollTo(scroll);
  349. if (!_lastMousePosition) {
  350. _lastMousePosition = QCursor::pos();
  351. }
  352. selectByMouse(*_lastMousePosition);
  353. update();
  354. }
  355. }
  356. void SuggestionsWidget::paintEvent(QPaintEvent *e) {
  357. auto p = QPainter(this);
  358. _repaintScheduled = false;
  359. const auto clip = e->rect();
  360. p.fillRect(clip, _st.bg);
  361. const auto shift = innerShift();
  362. p.translate(-shift);
  363. const auto paint = clip.translated(shift);
  364. const auto from = std::max(paint.x(), 0) / _oneWidth;
  365. const auto till = std::min(
  366. (paint.x() + paint.width() + _oneWidth - 1) / _oneWidth,
  367. int(_rows.size()));
  368. const auto selected = (_pressed >= 0)
  369. ? _pressed
  370. : _selectedAnimation.value(_selected);
  371. if (selected > -1.) {
  372. _overRect.paint(
  373. p,
  374. QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth));
  375. }
  376. auto context = Ui::CustomEmoji::Context{
  377. .textColor = _st.textFg->c,
  378. .now = crl::now(),
  379. };
  380. for (auto i = from; i != till; ++i) {
  381. const auto &row = _rows[i];
  382. const auto emoji = row.emoji;
  383. const auto esize = Ui::Emoji::GetSizeLarge();
  384. const auto size = esize / style::DevicePixelRatio();
  385. const auto x = i * _oneWidth + (_oneWidth - size) / 2;
  386. const auto y = (_oneWidth - size) / 2;
  387. if (row.custom) {
  388. context.position = { x, y };
  389. row.custom->paint(p, context);
  390. } else {
  391. Ui::Emoji::Draw(p, emoji, esize, x, y);
  392. }
  393. }
  394. paintFadings(p);
  395. }
  396. void SuggestionsWidget::paintFadings(QPainter &p) const {
  397. const auto scroll = scrollCurrent();
  398. const auto o_left = std::clamp(
  399. scroll / float64(st::emojiSuggestionsFadeAfter),
  400. 0.,
  401. 1.);
  402. const auto shift = innerShift();
  403. if (o_left > 0.) {
  404. p.setOpacity(o_left);
  405. const auto rect = myrtlrect(
  406. shift.x(),
  407. 0,
  408. _st.fadeLeft.width(),
  409. height());
  410. _st.fadeLeft.fill(p, rect);
  411. p.setOpacity(1.);
  412. }
  413. const auto o_right = std::clamp(
  414. (_scrollMax - scroll) / float64(st::emojiSuggestionsFadeAfter),
  415. 0.,
  416. 1.);
  417. if (o_right > 0.) {
  418. p.setOpacity(o_right);
  419. const auto rect = myrtlrect(
  420. shift.x() + width() - _st.fadeRight.width(),
  421. 0,
  422. _st.fadeRight.width(),
  423. height());
  424. _st.fadeRight.fill(p, rect);
  425. p.setOpacity(1.);
  426. }
  427. }
  428. void SuggestionsWidget::keyPressEvent(QKeyEvent *e) {
  429. handleKeyEvent(e->key());
  430. }
  431. bool SuggestionsWidget::handleKeyEvent(int key) {
  432. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  433. return triggerSelectedRow();
  434. } else if (key == Qt::Key_Tab) {
  435. if (_selected < 0 || _selected >= _rows.size()) {
  436. setSelected(0);
  437. }
  438. return triggerSelectedRow();
  439. } else if (_rows.empty()
  440. || (key != Qt::Key_Up
  441. && key != Qt::Key_Down
  442. && key != Qt::Key_Left
  443. && key != Qt::Key_Right)) {
  444. return false;
  445. }
  446. const auto delta = (key == Qt::Key_Down || key == Qt::Key_Right)
  447. ? 1
  448. : -1;
  449. if (delta < 0 && _selected < 0) {
  450. return false;
  451. }
  452. auto start = _selected;
  453. if (start < 0 || start >= _rows.size()) {
  454. start = (delta > 0) ? (_rows.size() - 1) : 0;
  455. }
  456. auto newSelected = start + delta;
  457. if (newSelected < 0) {
  458. newSelected = -1;
  459. } else if (newSelected >= _rows.size()) {
  460. newSelected -= _rows.size();
  461. }
  462. _mouseSelection = false;
  463. _lastMousePosition = std::nullopt;
  464. setSelected(newSelected, anim::type::normal);
  465. return true;
  466. }
  467. void SuggestionsWidget::setSelected(int selected, anim::type animated) {
  468. if (selected >= _rows.size()) {
  469. selected = -1;
  470. }
  471. if (animated == anim::type::normal) {
  472. _selectedAnimation.start(
  473. [=] { update(); },
  474. _selected,
  475. selected,
  476. st::universalDuration,
  477. anim::sineInOut);
  478. if (_scrollMax > 0) {
  479. const auto selectedMax = int(_rows.size()) - 3;
  480. const auto selectedForScroll = std::min(
  481. std::max(selected, 1) - 1,
  482. selectedMax);
  483. scrollTo((_scrollMax * selectedForScroll) / selectedMax, animated);
  484. }
  485. } else if (_selectedAnimation.animating()) {
  486. _selectedAnimation.stop();
  487. update();
  488. }
  489. if (_selected != selected) {
  490. updateSelectedItem();
  491. _selected = selected;
  492. updateSelectedItem();
  493. }
  494. }
  495. int SuggestionsWidget::scrollCurrent() const {
  496. return _scrollAnimation.value(_scrollValue);
  497. }
  498. void SuggestionsWidget::scrollTo(int value, anim::type animated) {
  499. if (animated == anim::type::instant) {
  500. _scrollAnimation.stop();
  501. } else {
  502. _scrollAnimation.start(
  503. [=] { update(); },
  504. _scrollValue,
  505. value,
  506. st::universalDuration,
  507. anim::sineInOut);
  508. }
  509. _scrollValue = value;
  510. update();
  511. }
  512. void SuggestionsWidget::stopAnimations() {
  513. _scrollValue = _scrollAnimation.value(_scrollValue);
  514. _scrollAnimation.stop();
  515. }
  516. void SuggestionsWidget::setPressed(int pressed) {
  517. if (pressed >= _rows.size()) {
  518. pressed = -1;
  519. }
  520. if (_pressed != pressed) {
  521. _pressed = pressed;
  522. if (_pressed >= 0) {
  523. _mousePressPosition = QCursor::pos();
  524. }
  525. }
  526. }
  527. void SuggestionsWidget::clearMouseSelection() {
  528. if (_mouseSelection) {
  529. clearSelection();
  530. }
  531. }
  532. void SuggestionsWidget::clearSelection() {
  533. _mouseSelection = false;
  534. _lastMousePosition = std::nullopt;
  535. setSelected(-1);
  536. }
  537. void SuggestionsWidget::updateItem(int index) {
  538. if (index >= 0 && index < _rows.size()) {
  539. update(
  540. _padding.left() + index * _oneWidth - scrollCurrent(),
  541. _padding.top(),
  542. _oneWidth,
  543. _oneWidth);
  544. }
  545. }
  546. void SuggestionsWidget::updateSelectedItem() {
  547. updateItem(_selected);
  548. }
  549. QRect SuggestionsWidget::inner() const {
  550. return QRect(0, 0, _rows.size() * _oneWidth, _oneWidth);
  551. }
  552. QPoint SuggestionsWidget::innerShift() const {
  553. return QPoint(scrollCurrent() - _padding.left(), -_padding.top());
  554. }
  555. QPoint SuggestionsWidget::mapToInner(QPoint globalPosition) const {
  556. return mapFromGlobal(globalPosition) + innerShift();
  557. }
  558. void SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) {
  559. const auto globalPosition = e->globalPos();
  560. if (_dragScrollStart >= 0) {
  561. const auto delta = (_mousePressPosition.x() - globalPosition.x());
  562. const auto scroll = std::clamp(
  563. _dragScrollStart + (rtl() ? -1 : 1) * delta,
  564. 0,
  565. _scrollMax);
  566. if (scrollCurrent() != scroll) {
  567. scrollTo(scroll);
  568. update();
  569. }
  570. return;
  571. } else if ((_pressed >= 0)
  572. && (_scrollMax > 0)
  573. && ((_mousePressPosition - globalPosition).manhattanLength()
  574. >= QApplication::startDragDistance())) {
  575. _dragScrollStart = scrollCurrent();
  576. _mousePressPosition = globalPosition;
  577. scrollTo(_dragScrollStart);
  578. }
  579. if (inner().contains(mapToInner(globalPosition))) {
  580. if (!_lastMousePosition) {
  581. _lastMousePosition = globalPosition;
  582. return;
  583. } else if (!_mouseSelection
  584. && *_lastMousePosition == globalPosition) {
  585. return;
  586. }
  587. selectByMouse(globalPosition);
  588. } else {
  589. clearMouseSelection();
  590. }
  591. }
  592. void SuggestionsWidget::selectByMouse(QPoint globalPosition) {
  593. _mouseSelection = true;
  594. _lastMousePosition = globalPosition;
  595. const auto p = mapToInner(globalPosition);
  596. const auto selected = (p.x() >= 0) ? (p.x() / _oneWidth) : -1;
  597. setSelected((selected >= 0 && selected < _rows.size()) ? selected : -1);
  598. }
  599. void SuggestionsWidget::mousePressEvent(QMouseEvent *e) {
  600. selectByMouse(e->globalPos());
  601. if (_selected >= 0) {
  602. setPressed(_selected);
  603. }
  604. }
  605. void SuggestionsWidget::mouseReleaseEvent(QMouseEvent *e) {
  606. if (_pressed >= 0) {
  607. const auto pressed = _pressed;
  608. setPressed(-1);
  609. if (_dragScrollStart >= 0) {
  610. _dragScrollStart = -1;
  611. } else if (pressed == _selected) {
  612. triggerRow(_rows[_selected]);
  613. }
  614. }
  615. }
  616. bool SuggestionsWidget::triggerSelectedRow() const {
  617. if (_selected >= 0) {
  618. triggerRow(_rows[_selected]);
  619. return true;
  620. }
  621. return false;
  622. }
  623. void SuggestionsWidget::triggerRow(const Row &row) const {
  624. _triggered.fire({
  625. row.emoji->text(),
  626. (row.document
  627. ? Data::SerializeCustomEmojiId(row.document)
  628. : QString()),
  629. });
  630. }
  631. void SuggestionsWidget::enterEventHook(QEnterEvent *e) {
  632. if (!inner().contains(mapToInner(QCursor::pos()))) {
  633. clearMouseSelection();
  634. }
  635. return TWidget::enterEventHook(e);
  636. }
  637. void SuggestionsWidget::leaveEventHook(QEvent *e) {
  638. clearMouseSelection();
  639. return TWidget::leaveEventHook(e);
  640. }
  641. SuggestionsController::SuggestionsController(
  642. not_null<QWidget*> parent,
  643. not_null<QWidget*> outer,
  644. not_null<QTextEdit*> field,
  645. not_null<Main::Session*> session,
  646. const Options &options)
  647. : QObject(parent)
  648. , _st(options.st ? *options.st : st::defaultEmojiSuggestions)
  649. , _field(field)
  650. , _session(session)
  651. , _showExactTimer([=] { showWithQuery(getEmojiQuery()); })
  652. , _options(options) {
  653. _container = base::make_unique_q<InnerDropdown>(outer, _st.dropdown);
  654. _container->setAutoHiding(false);
  655. _suggestions = _container->setOwnedWidget(
  656. object_ptr<Ui::Emoji::SuggestionsWidget>(
  657. _container,
  658. _st,
  659. session,
  660. _options.suggestCustomEmoji,
  661. _options.allowCustomWithoutPremium));
  662. setReplaceCallback(nullptr);
  663. const auto fieldCallback = [=](not_null<QEvent*> event) {
  664. return (_container && fieldFilter(event))
  665. ? base::EventFilterResult::Cancel
  666. : base::EventFilterResult::Continue;
  667. };
  668. _fieldFilter.reset(base::install_event_filter(_field, fieldCallback));
  669. const auto outerCallback = [=](not_null<QEvent*> event) {
  670. return (_container && outerFilter(event))
  671. ? base::EventFilterResult::Cancel
  672. : base::EventFilterResult::Continue;
  673. };
  674. _outerFilter.reset(base::install_event_filter(outer, outerCallback));
  675. QObject::connect(
  676. _field,
  677. &QTextEdit::textChanged,
  678. _container,
  679. [=] { handleTextChange(); });
  680. QObject::connect(
  681. _field,
  682. &QTextEdit::cursorPositionChanged,
  683. _container,
  684. [=] { handleCursorPositionChange(); });
  685. _suggestions->toggleAnimated(
  686. ) | rpl::start_with_next([=](bool visible) {
  687. suggestionsUpdated(visible);
  688. }, _lifetime);
  689. _suggestions->triggered(
  690. ) | rpl::start_with_next([=](const SuggestionsWidget::Chosen &chosen) {
  691. replaceCurrent(chosen.emoji, chosen.customData);
  692. }, _lifetime);
  693. Core::App().emojiKeywords().refreshed(
  694. ) | rpl::start_with_next([=] {
  695. _keywordsRefreshed = true;
  696. if (!_showExactTimer.isActive()) {
  697. showWithQuery(_lastShownQuery);
  698. }
  699. }, _lifetime);
  700. updateForceHidden();
  701. _container->shownValue(
  702. ) | rpl::filter([=](bool shown) {
  703. return shown && !_shown;
  704. }) | rpl::start_with_next([=] {
  705. _container->hide();
  706. }, _container->lifetime());
  707. handleTextChange();
  708. }
  709. not_null<SuggestionsController*> SuggestionsController::Init(
  710. not_null<QWidget*> outer,
  711. not_null<Ui::InputField*> field,
  712. not_null<Main::Session*> session,
  713. const Options &options) {
  714. const auto result = Ui::CreateChild<SuggestionsController>(
  715. field.get(),
  716. outer,
  717. field->rawTextEdit(),
  718. session,
  719. options);
  720. result->setReplaceCallback([=](
  721. int from,
  722. int till,
  723. const QString &replacement,
  724. const QString &customEmojiData) {
  725. field->commitInstantReplacement(
  726. from,
  727. till,
  728. replacement,
  729. customEmojiData);
  730. });
  731. return result;
  732. }
  733. void SuggestionsController::setReplaceCallback(
  734. Fn<void(
  735. int from,
  736. int till,
  737. const QString &replacement,
  738. const QString &customEmojiData)> callback) {
  739. if (callback) {
  740. _replaceCallback = std::move(callback);
  741. } else {
  742. _replaceCallback = [=](
  743. int from,
  744. int till,
  745. const QString &replacement,
  746. const QString &customEmojiData) {
  747. auto cursor = _field->textCursor();
  748. cursor.setPosition(from);
  749. cursor.setPosition(till, QTextCursor::KeepAnchor);
  750. cursor.insertText(replacement);
  751. };
  752. }
  753. }
  754. void SuggestionsController::handleTextChange() {
  755. if (Core::App().settings().suggestEmoji()
  756. && _field->textCursor().position() > 0) {
  757. Core::App().emojiKeywords().refresh();
  758. }
  759. _ignoreCursorPositionChange = true;
  760. InvokeQueued(_container, [=] { _ignoreCursorPositionChange = false; });
  761. const auto query = getEmojiQuery();
  762. if (v::is<EmojiPtr>(query)) {
  763. showWithQuery(query);
  764. InvokeQueued(_container, [=] {
  765. if (_shown) {
  766. updateGeometry();
  767. }
  768. });
  769. return;
  770. }
  771. const auto text = v::get<QString>(query);
  772. if (text.isEmpty() || _textChangeAfterKeyPress) {
  773. const auto exact = !text.isEmpty() && (text[0] != ':');
  774. if (exact) {
  775. const auto hidden = _container->isHidden()
  776. || _container->isHiding();
  777. _showExactTimer.callOnce(hidden ? kShowExactDelay : 0);
  778. } else {
  779. showWithQuery(query);
  780. _suggestions->selectFirstResult();
  781. }
  782. }
  783. }
  784. void SuggestionsController::showWithQuery(SuggestionsQuery query) {
  785. _showExactTimer.cancel();
  786. const auto force = base::take(_keywordsRefreshed);
  787. _lastShownQuery = query;
  788. _suggestions->showWithQuery(_lastShownQuery, force);
  789. _container->resizeToContent();
  790. }
  791. SuggestionsQuery SuggestionsController::getEmojiQuery() {
  792. if (!Core::App().settings().suggestEmoji()) {
  793. return QString();
  794. }
  795. const auto cursor = _field->textCursor();
  796. if (cursor.hasSelection()) {
  797. return QString();
  798. }
  799. const auto modernLimit = Core::App().emojiKeywords().maxQueryLength();
  800. const auto legacyLimit = GetSuggestionMaxLength();
  801. const auto position = cursor.position();
  802. const auto findTextPart = [&]() -> SuggestionsQuery {
  803. auto previousFragmentStart = 0;
  804. auto previousFragmentName = QString();
  805. auto document = _field->document();
  806. auto block = document->findBlock(position);
  807. for (auto i = block.begin(); !i.atEnd(); ++i) {
  808. auto fragment = i.fragment();
  809. if (!fragment.isValid()) {
  810. continue;
  811. }
  812. auto from = fragment.position();
  813. auto till = from + fragment.length();
  814. const auto format = fragment.charFormat();
  815. if (format.objectType() == InputField::kCustomEmojiFormat) {
  816. previousFragmentName = QString();
  817. continue;
  818. } else if (format.isImageFormat()) {
  819. const auto imageName = format.toImageFormat().name();
  820. if (from >= position || till < position) {
  821. previousFragmentStart = from;
  822. previousFragmentName = imageName;
  823. continue;
  824. } else if (const auto emoji = Emoji::FromUrl(imageName)) {
  825. _queryStartPosition = position - 1;
  826. const auto start = (previousFragmentName == imageName)
  827. ? previousFragmentStart
  828. : from;
  829. _emojiQueryLength = (position - start);
  830. return emoji;
  831. } else {
  832. continue;
  833. }
  834. }
  835. if (from >= position || till < position) {
  836. previousFragmentName = QString();
  837. continue;
  838. }
  839. _queryStartPosition = from;
  840. _emojiQueryLength = 0;
  841. return fragment.text();
  842. }
  843. return QString();
  844. };
  845. const auto part = findTextPart();
  846. if (const auto emoji = std::get_if<EmojiPtr>(&part)) {
  847. return *emoji;
  848. }
  849. const auto text = v::get<QString>(part);
  850. if (text.isEmpty()) {
  851. return QString();
  852. }
  853. const auto length = position - _queryStartPosition;
  854. for (auto i = length; i != 0;) {
  855. if (text[--i] == ':') {
  856. const auto previous = (i > 0) ? text[i - 1] : QChar(0);
  857. if (i > 0 && (previous.isLetter() || previous.isDigit())) {
  858. return QString();
  859. } else if (i + 1 == length || text[i + 1].isSpace()) {
  860. return QString();
  861. }
  862. _queryStartPosition += i + 2;
  863. return text.mid(i, length - i);
  864. }
  865. if (length - i > legacyLimit && length - i > modernLimit) {
  866. return QString();
  867. }
  868. }
  869. // Exact query should be full input field value.
  870. const auto end = [&] {
  871. auto cursor = _field->textCursor();
  872. cursor.movePosition(QTextCursor::End);
  873. return cursor.position();
  874. }();
  875. if (!_options.suggestExactFirstWord
  876. || !length
  877. || text[0].isSpace()
  878. || (length > modernLimit)
  879. || (_queryStartPosition != 0)
  880. || (position != end)) {
  881. return QString();
  882. }
  883. return text;
  884. }
  885. void SuggestionsController::replaceCurrent(
  886. const QString &replacement,
  887. const QString &customEmojiData) {
  888. const auto cursor = _field->textCursor();
  889. const auto position = cursor.position();
  890. const auto suggestion = getEmojiQuery();
  891. if (v::is<EmojiPtr>(suggestion)) {
  892. const auto weak = Ui::MakeWeak(_container.get());
  893. const auto count = std::max(_emojiQueryLength, 1);
  894. for (auto i = 0; i != count; ++i) {
  895. const auto start = position - count + i;
  896. _replaceCallback(start, start + 1, replacement, customEmojiData);
  897. if (!weak) {
  898. return;
  899. }
  900. }
  901. } else if (v::get<QString>(suggestion).isEmpty()) {
  902. showWithQuery(QString());
  903. } else {
  904. const auto from = position - v::get<QString>(suggestion).size();
  905. _replaceCallback(from, position, replacement, customEmojiData);
  906. }
  907. }
  908. void SuggestionsController::handleCursorPositionChange() {
  909. InvokeQueued(_container, [=] {
  910. if (_ignoreCursorPositionChange) {
  911. return;
  912. }
  913. showWithQuery(QString());
  914. });
  915. }
  916. void SuggestionsController::suggestionsUpdated(bool visible) {
  917. _shown = visible;
  918. if (_shown) {
  919. _container->resizeToContent();
  920. updateGeometry();
  921. if (!_forceHidden) {
  922. if (_container->isHidden() || _container->isHiding()) {
  923. raise();
  924. }
  925. _container->showAnimated(
  926. Ui::PanelAnimation::Origin::BottomLeft);
  927. }
  928. } else if (!_forceHidden) {
  929. _container->hideAnimated();
  930. }
  931. }
  932. void SuggestionsController::updateGeometry() {
  933. auto cursor = _field->textCursor();
  934. cursor.setPosition(_queryStartPosition);
  935. auto aroundRect = _field->cursorRect(cursor);
  936. aroundRect.setTopLeft(_field->viewport()->mapToGlobal(aroundRect.topLeft()));
  937. aroundRect.setTopLeft(_container->parentWidget()->mapFromGlobal(aroundRect.topLeft()));
  938. auto boundingRect = _container->parentWidget()->rect();
  939. auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft;
  940. auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft();
  941. const auto padding = _st.dropdown.padding;
  942. const auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2;
  943. point -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _container->height());
  944. if (rtl()) {
  945. if (point.x() < boundingRect.x()) {
  946. point.setX(boundingRect.x());
  947. }
  948. if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {
  949. point.setX(boundingRect.x() + boundingRect.width() - _container->width());
  950. }
  951. } else {
  952. if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {
  953. point.setX(boundingRect.x() + boundingRect.width() - _container->width());
  954. }
  955. if (point.x() < boundingRect.x()) {
  956. point.setX(boundingRect.x());
  957. }
  958. }
  959. if (point.y() < boundingRect.y()) {
  960. point.setY(aroundRect.y() + aroundRect.height());
  961. origin = (origin == PanelAnimation::Origin::BottomRight) ? PanelAnimation::Origin::TopRight : PanelAnimation::Origin::TopLeft;
  962. }
  963. _container->move(point);
  964. }
  965. void SuggestionsController::updateForceHidden() {
  966. _forceHidden = !_field->isVisible() || !_field->hasFocus();
  967. if (_forceHidden) {
  968. _container->hideFast();
  969. } else if (_shown) {
  970. _container->showFast();
  971. }
  972. }
  973. bool SuggestionsController::fieldFilter(not_null<QEvent*> event) {
  974. auto type = event->type();
  975. switch (type) {
  976. case QEvent::Move:
  977. case QEvent::Resize: {
  978. if (_shown) {
  979. updateGeometry();
  980. }
  981. } break;
  982. case QEvent::Show:
  983. case QEvent::ShowToParent:
  984. case QEvent::Hide:
  985. case QEvent::HideToParent:
  986. case QEvent::FocusIn:
  987. case QEvent::FocusOut: {
  988. updateForceHidden();
  989. } break;
  990. case QEvent::KeyPress: {
  991. const auto key = static_cast<QKeyEvent*>(event.get())->key();
  992. switch (key) {
  993. case Qt::Key_Enter:
  994. case Qt::Key_Return:
  995. case Qt::Key_Tab:
  996. case Qt::Key_Up:
  997. case Qt::Key_Down:
  998. case Qt::Key_Left:
  999. case Qt::Key_Right:
  1000. if (_shown && !_forceHidden) {
  1001. return _suggestions->handleKeyEvent(key);
  1002. }
  1003. break;
  1004. case Qt::Key_Escape:
  1005. if (_shown && !_forceHidden) {
  1006. showWithQuery(QString());
  1007. return true;
  1008. }
  1009. break;
  1010. }
  1011. _textChangeAfterKeyPress = true;
  1012. InvokeQueued(_container, [=] { _textChangeAfterKeyPress = false; });
  1013. } break;
  1014. }
  1015. return false;
  1016. }
  1017. bool SuggestionsController::outerFilter(not_null<QEvent*> event) {
  1018. auto type = event->type();
  1019. switch (type) {
  1020. case QEvent::Move:
  1021. case QEvent::Resize: {
  1022. // updateGeometry uses not only container geometry, but also
  1023. // container children geometries that will be updated later.
  1024. InvokeQueued(_container, [=] {
  1025. if (_shown) {
  1026. updateGeometry();
  1027. }
  1028. });
  1029. } break;
  1030. }
  1031. return false;
  1032. }
  1033. void SuggestionsController::raise() {
  1034. _container->raise();
  1035. }
  1036. } // namespace Emoji
  1037. } // namespace Ui