1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "chat_helpers/field_autocomplete.h"
9
10 #include "data/data_document.h"
11 #include "data/data_document_media.h"
12 #include "data/data_channel.h"
13 #include "data/data_chat.h"
14 #include "data/data_user.h"
15 #include "data/data_peer_values.h"
16 #include "data/data_file_origin.h"
17 #include "data/data_session.h"
18 #include "data/stickers/data_stickers.h"
19 #include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu
20 #include "chat_helpers/stickers_lottie.h"
21 #include "chat_helpers/message_field.h" // PrepareMentionTag.
22 #include "mainwindow.h"
23 #include "apiwrap.h"
24 #include "main/main_session.h"
25 #include "storage/storage_account.h"
26 #include "core/application.h"
27 #include "core/core_settings.h"
28 #include "lottie/lottie_single_player.h"
29 #include "ui/widgets/popup_menu.h"
30 #include "ui/widgets/scroll_area.h"
31 #include "ui/widgets/input_fields.h"
32 #include "ui/text/text_options.h"
33 #include "ui/image/image.h"
34 #include "ui/effects/path_shift_gradient.h"
35 #include "ui/ui_utility.h"
36 #include "ui/cached_round_corners.h"
37 #include "base/unixtime.h"
38 #include "base/random.h"
39 #include "window/window_adaptive.h"
40 #include "window/window_session_controller.h"
41 #include "styles/style_chat.h"
42 #include "styles/style_widgets.h"
43 #include "styles/style_chat_helpers.h"
44 #include "base/qt_adapters.h"
45
46 #include <QtWidgets/QApplication>
47
48 class FieldAutocomplete::Inner final : public Ui::RpWidget {
49 public:
50 struct ScrollTo {
51 int top;
52 int bottom;
53 };
54
55 Inner(
56 not_null<Window::SessionController*> controller,
57 not_null<FieldAutocomplete*> parent,
58 not_null<MentionRows*> mrows,
59 not_null<HashtagRows*> hrows,
60 not_null<BotCommandRows*> brows,
61 not_null<StickerRows*> srows);
62
63 void clearSel(bool hidden = false);
64 bool moveSel(int key);
65 bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
66 bool chooseAtIndex(
67 FieldAutocomplete::ChooseMethod method,
68 int index,
69 Api::SendOptions options = Api::SendOptions()) const;
70
71 void setRecentInlineBotsInRows(int32 bots);
72 void setSendMenuType(Fn<SendMenu::Type()> &&callback);
73 void rowsUpdated();
74
75 rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
76 rpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;
77 rpl::producer<FieldAutocomplete::BotCommandChosen>
78 botCommandChosen() const;
79 rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
80 rpl::producer<ScrollTo> scrollToRequested() const;
81
82 void onParentGeometryChanged();
83
84 private:
85 void paintEvent(QPaintEvent *e) override;
86 void resizeEvent(QResizeEvent *e) override;
87
88 void enterEventHook(QEnterEvent *e) override;
89 void leaveEventHook(QEvent *e) override;
90
91 void mousePressEvent(QMouseEvent *e) override;
92 void mouseMoveEvent(QMouseEvent *e) override;
93 void mouseReleaseEvent(QMouseEvent *e) override;
94 void contextMenuEvent(QContextMenuEvent *e) override;
95
96 void updateSelectedRow();
97 void setSel(int sel, bool scroll = false);
98 void showPreview();
99 void selectByMouse(QPoint global);
100
101 QSize stickerBoundingBox() const;
102 void setupLottie(StickerSuggestion &suggestion);
103 void repaintSticker(not_null<DocumentData*> document);
104 std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
105
106 const not_null<Window::SessionController*> _controller;
107 const not_null<FieldAutocomplete*> _parent;
108 const not_null<MentionRows*> _mrows;
109 const not_null<HashtagRows*> _hrows;
110 const not_null<BotCommandRows*> _brows;
111 const not_null<StickerRows*> _srows;
112 rpl::lifetime _stickersLifetime;
113 std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
114 base::unique_qptr<Ui::PopupMenu> _menu;
115 int _stickersPerRow = 1;
116 int _recentInlineBotsInRows = 0;
117 int _sel = -1;
118 int _down = -1;
119 std::optional<QPoint> _lastMousePosition;
120 bool _mouseSelection = false;
121
122 bool _overDelete = false;
123
124 bool _previewShown = false;
125
126 bool _isOneColumn = false;
127
128 const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
129
130 Fn<SendMenu::Type()> _sendMenuType;
131
132 rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
133 rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
134 rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
135 rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
136 rpl::event_stream<ScrollTo> _scrollToRequested;
137
138 base::Timer _previewTimer;
139
140 };
141
FieldAutocomplete(QWidget * parent,not_null<Window::SessionController * > controller)142 FieldAutocomplete::FieldAutocomplete(
143 QWidget *parent,
144 not_null<Window::SessionController*> controller)
145 : RpWidget(parent)
146 , _controller(controller)
147 , _scroll(this) {
148 hide();
149
150 _scroll->setGeometry(rect());
151
152 _inner = _scroll->setOwnedWidget(
153 object_ptr<Inner>(
154 _controller,
155 this,
156 &_mrows,
157 &_hrows,
158 &_brows,
159 &_srows));
160 _inner->setGeometry(rect());
161
162 _inner->scrollToRequested(
163 ) | rpl::start_with_next([=](Inner::ScrollTo data) {
164 _scroll->scrollToY(data.top, data.bottom);
165 }, lifetime());
166
167 _scroll->show();
168 _inner->show();
169
170 hide();
171
172 _scroll->geometryChanged(
173 ) | rpl::start_with_next(crl::guard(_inner, [=] {
174 _inner->onParentGeometryChanged();
175 }), lifetime());
176 }
177
controller() const178 not_null<Window::SessionController*> FieldAutocomplete::controller() const {
179 return _controller;
180 }
181
mentionChosen() const182 auto FieldAutocomplete::mentionChosen() const
183 -> rpl::producer<FieldAutocomplete::MentionChosen> {
184 return _inner->mentionChosen();
185 }
186
hashtagChosen() const187 auto FieldAutocomplete::hashtagChosen() const
188 -> rpl::producer<FieldAutocomplete::HashtagChosen> {
189 return _inner->hashtagChosen();
190 }
191
botCommandChosen() const192 auto FieldAutocomplete::botCommandChosen() const
193 -> rpl::producer<FieldAutocomplete::BotCommandChosen> {
194 return _inner->botCommandChosen();
195 }
196
stickerChosen() const197 auto FieldAutocomplete::stickerChosen() const
198 -> rpl::producer<FieldAutocomplete::StickerChosen> {
199 return _inner->stickerChosen();
200 }
201
choosingProcesses() const202 auto FieldAutocomplete::choosingProcesses() const
203 -> rpl::producer<FieldAutocomplete::Type> {
204 return _scroll->scrollTopChanges(
205 ) | rpl::filter([](int top) {
206 return top != 0;
207 }) | rpl::map([=] {
208 return !_mrows.empty()
209 ? Type::Mentions
210 : !_hrows.empty()
211 ? Type::Hashtags
212 : !_brows.empty()
213 ? Type::BotCommands
214 : !_srows.empty()
215 ? Type::Stickers
216 : _type;
217 });
218 }
219
220 FieldAutocomplete::~FieldAutocomplete() = default;
221
paintEvent(QPaintEvent * e)222 void FieldAutocomplete::paintEvent(QPaintEvent *e) {
223 Painter p(this);
224
225 auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
226 if (opacity < 1.) {
227 if (opacity > 0.) {
228 p.setOpacity(opacity);
229 p.drawPixmap(0, 0, _cache);
230 } else if (_hiding) {
231
232 }
233 return;
234 }
235
236 p.fillRect(rect(), st::mentionBg);
237 }
238
showFiltered(not_null<PeerData * > peer,QString query,bool addInlineBots)239 void FieldAutocomplete::showFiltered(
240 not_null<PeerData*> peer,
241 QString query,
242 bool addInlineBots) {
243 _chat = peer->asChat();
244 _user = peer->asUser();
245 _channel = peer->asChannel();
246 if (query.isEmpty()) {
247 _type = Type::Mentions;
248 rowsUpdated(
249 MentionRows(),
250 HashtagRows(),
251 BotCommandRows(),
252 base::take(_srows),
253 false);
254 return;
255 }
256
257 _emoji = nullptr;
258
259 query = query.toLower();
260 auto type = Type::Stickers;
261 auto plainQuery = QStringView(query);
262 switch (query.at(0).unicode()) {
263 case '@':
264 type = Type::Mentions;
265 plainQuery = base::StringViewMid(query, 1);
266 break;
267 case '#':
268 type = Type::Hashtags;
269 plainQuery = base::StringViewMid(query, 1);
270 break;
271 case '/':
272 type = Type::BotCommands;
273 plainQuery = base::StringViewMid(query, 1);
274 break;
275 }
276 bool resetScroll = (_type != type || _filter != plainQuery);
277 if (resetScroll) {
278 _type = type;
279 _filter = TextUtilities::RemoveAccents(plainQuery.toString());
280 }
281 _addInlineBots = addInlineBots;
282
283 updateFiltered(resetScroll);
284 }
285
showStickers(EmojiPtr emoji)286 void FieldAutocomplete::showStickers(EmojiPtr emoji) {
287 bool resetScroll = (_emoji != emoji);
288 _emoji = emoji;
289 _type = Type::Stickers;
290 if (!emoji) {
291 rowsUpdated(
292 base::take(_mrows),
293 base::take(_hrows),
294 base::take(_brows),
295 StickerRows(),
296 false);
297 return;
298 }
299
300 _chat = nullptr;
301 _user = nullptr;
302 _channel = nullptr;
303
304 updateFiltered(resetScroll);
305 }
306
clearFilteredBotCommands()307 bool FieldAutocomplete::clearFilteredBotCommands() {
308 if (_brows.empty()) {
309 return false;
310 }
311 _brows.clear();
312 return true;
313 }
314
315 namespace {
316 template <typename T, typename U>
indexOfInFirstN(const T & v,const U & elem,int last)317 inline int indexOfInFirstN(const T &v, const U &elem, int last) {
318 for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last); i != e; ++i) {
319 if (i->user == elem) {
320 return (i - b);
321 }
322 }
323 return -1;
324 }
325 }
326
getStickerSuggestions()327 FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
328 const auto list = _controller->session().data().stickers().getListByEmoji(
329 _emoji,
330 _stickersSeed
331 );
332 auto result = ranges::views::all(
333 list
334 ) | ranges::views::transform([](not_null<DocumentData*> sticker) {
335 return StickerSuggestion{
336 sticker,
337 sticker->createMediaView()
338 };
339 }) | ranges::to_vector;
340 for (auto &suggestion : _srows) {
341 if (!suggestion.animated) {
342 continue;
343 }
344 const auto i = ranges::find(
345 result,
346 suggestion.document,
347 &StickerSuggestion::document);
348 if (i != end(result)) {
349 i->animated = std::move(suggestion.animated);
350 }
351 }
352 return result;
353 }
354
updateFiltered(bool resetScroll)355 void FieldAutocomplete::updateFiltered(bool resetScroll) {
356 int32 now = base::unixtime::now(), recentInlineBots = 0;
357 MentionRows mrows;
358 HashtagRows hrows;
359 BotCommandRows brows;
360 StickerRows srows;
361 if (_emoji) {
362 srows = getStickerSuggestions();
363 } else if (_type == Type::Mentions) {
364 int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0;
365 if (_chat) {
366 maxListSize += (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size());
367 } else if (_channel && _channel->isMegagroup()) {
368 if (!_channel->lastParticipantsRequestNeeded()) {
369 maxListSize += _channel->mgInfo->lastParticipants.size();
370 }
371 }
372 if (maxListSize) {
373 mrows.reserve(maxListSize);
374 }
375
376 auto filterNotPassedByUsername = [this](UserData *user) -> bool {
377 if (user->username.startsWith(_filter, Qt::CaseInsensitive)) {
378 bool exactUsername = (user->username.size() == _filter.size());
379 return exactUsername;
380 }
381 return true;
382 };
383 auto filterNotPassedByName = [&](UserData *user) -> bool {
384 for (const auto &nameWord : user->nameWords()) {
385 if (nameWord.startsWith(_filter, Qt::CaseInsensitive)) {
386 auto exactUsername = (user->username.compare(_filter, Qt::CaseInsensitive) == 0);
387 return exactUsername;
388 }
389 }
390 return filterNotPassedByUsername(user);
391 };
392
393 bool listAllSuggestions = _filter.isEmpty();
394 if (_addInlineBots) {
395 for (const auto user : cRecentInlineBots()) {
396 if (user->isInaccessible()
397 || (!listAllSuggestions
398 && filterNotPassedByUsername(user))) {
399 continue;
400 }
401 mrows.push_back({ user });
402 ++recentInlineBots;
403 }
404 }
405 if (_chat) {
406 auto sorted = base::flat_multi_map<TimeId, not_null<UserData*>>();
407 const auto byOnline = [&](not_null<UserData*> user) {
408 return Data::SortByOnlineValue(user, now);
409 };
410 mrows.reserve(mrows.size() + (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size()));
411 if (_chat->noParticipantInfo()) {
412 _chat->session().api().requestFullPeer(_chat);
413 } else if (!_chat->participants.empty()) {
414 for (const auto &user : _chat->participants) {
415 if (user->isInaccessible()) continue;
416 if (!listAllSuggestions && filterNotPassedByName(user)) continue;
417 if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
418 sorted.emplace(byOnline(user), user);
419 }
420 }
421 for (const auto user : _chat->lastAuthors) {
422 if (user->isInaccessible()) continue;
423 if (!listAllSuggestions && filterNotPassedByName(user)) continue;
424 if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
425 mrows.push_back({ user });
426 sorted.remove(byOnline(user), user);
427 }
428 for (auto i = sorted.cend(), b = sorted.cbegin(); i != b;) {
429 --i;
430 mrows.push_back({ i->second });
431 }
432 } else if (_channel && _channel->isMegagroup()) {
433 if (_channel->lastParticipantsRequestNeeded()) {
434 _channel->session().api().requestLastParticipants(_channel);
435 } else {
436 mrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size());
437 for (const auto user : _channel->mgInfo->lastParticipants) {
438 if (user->isInaccessible()) continue;
439 if (!listAllSuggestions && filterNotPassedByName(user)) continue;
440 if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
441 mrows.push_back({ user });
442 }
443 }
444 }
445 } else if (_type == Type::Hashtags) {
446 bool listAllSuggestions = _filter.isEmpty();
447 auto &recent(cRecentWriteHashtags());
448 hrows.reserve(recent.size());
449 for (const auto &item : recent) {
450 const auto &tag = item.first;
451 if (!listAllSuggestions
452 && (tag.size() == _filter.size()
453 || !TextUtilities::RemoveAccents(tag).startsWith(
454 _filter,
455 Qt::CaseInsensitive))) {
456 continue;
457 }
458 hrows.push_back(tag);
459 }
460 } else if (_type == Type::BotCommands) {
461 bool listAllSuggestions = _filter.isEmpty();
462 bool hasUsername = _filter.indexOf('@') > 0;
463 base::flat_map<
464 not_null<UserData*>,
465 not_null<const std::vector<BotCommand>*>> bots;
466 int32 cnt = 0;
467 if (_chat) {
468 if (_chat->noParticipantInfo()) {
469 _chat->session().api().requestFullPeer(_chat);
470 } else if (!_chat->participants.empty()) {
471 const auto &commands = _chat->botCommands();
472 for (const auto &user : _chat->participants) {
473 if (!user->isBot()) {
474 continue;
475 }
476 const auto i = commands.find(peerToUser(user->id));
477 if (i != end(commands)) {
478 bots.emplace(user, &i->second);
479 cnt += i->second.size();
480 }
481 }
482 }
483 } else if (_user && _user->isBot()) {
484 if (!_user->botInfo->inited) {
485 _user->session().api().requestFullPeer(_user);
486 }
487 cnt = _user->botInfo->commands.size();
488 bots.emplace(_user, &_user->botInfo->commands);
489 } else if (_channel && _channel->isMegagroup()) {
490 if (_channel->mgInfo->bots.empty()) {
491 if (!_channel->mgInfo->botStatus) {
492 _channel->session().api().requestBots(_channel);
493 }
494 } else {
495 const auto &commands = _channel->mgInfo->botCommands();
496 for (const auto &user : _channel->mgInfo->bots) {
497 if (!user->isBot()) {
498 continue;
499 }
500 const auto i = commands.find(peerToUser(user->id));
501 if (i != end(commands)) {
502 bots.emplace(user, &i->second);
503 cnt += i->second.size();
504 }
505 }
506 }
507 }
508 if (cnt) {
509 const auto make = [&](
510 not_null<UserData*> user,
511 const BotCommand &command) {
512 return BotCommandRow{
513 user,
514 command.command,
515 command.description,
516 user->activeUserpicView()
517 };
518 };
519 brows.reserve(cnt);
520 int32 botStatus = _chat ? _chat->botStatus : ((_channel && _channel->isMegagroup()) ? _channel->mgInfo->botStatus : -1);
521 if (_chat) {
522 for (const auto &user : _chat->lastAuthors) {
523 if (!user->isBot()) {
524 continue;
525 }
526 const auto i = bots.find(user);
527 if (i == end(bots)) {
528 continue;
529 }
530 for (const auto &command : *i->second) {
531 if (!listAllSuggestions) {
532 auto toFilter = (hasUsername || botStatus == 0 || botStatus == 2)
533 ? command.command + '@' + user->username
534 : command.command;
535 if (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) {
536 continue;
537 }
538 }
539 brows.push_back(make(user, command));
540 }
541 bots.erase(i);
542 }
543 }
544 if (!bots.empty()) {
545 for (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {
546 const auto user = i->first;
547 for (const auto &command : *i->second) {
548 if (!listAllSuggestions) {
549 QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? command.command + '@' + user->username : command.command;
550 if (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) continue;
551 }
552 brows.push_back(make(user, command));
553 }
554 }
555 }
556 }
557 }
558 rowsUpdated(
559 std::move(mrows),
560 std::move(hrows),
561 std::move(brows),
562 std::move(srows),
563 resetScroll);
564 _inner->setRecentInlineBotsInRows(recentInlineBots);
565 }
566
rowsUpdated(MentionRows && mrows,HashtagRows && hrows,BotCommandRows && brows,StickerRows && srows,bool resetScroll)567 void FieldAutocomplete::rowsUpdated(
568 MentionRows &&mrows,
569 HashtagRows &&hrows,
570 BotCommandRows &&brows,
571 StickerRows &&srows,
572 bool resetScroll) {
573 if (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) {
574 if (!isHidden()) {
575 hideAnimated();
576 }
577 _scroll->scrollToY(0);
578 _mrows.clear();
579 _hrows.clear();
580 _brows.clear();
581 _srows.clear();
582 } else {
583 _mrows = std::move(mrows);
584 _hrows = std::move(hrows);
585 _brows = std::move(brows);
586 _srows = std::move(srows);
587
588 bool hidden = _hiding || isHidden();
589 if (hidden) {
590 show();
591 _scroll->show();
592 }
593 recount(resetScroll);
594 update();
595 if (hidden) {
596 hide();
597 showAnimated();
598 }
599 }
600 _inner->rowsUpdated();
601 }
602
setBoundings(QRect boundings)603 void FieldAutocomplete::setBoundings(QRect boundings) {
604 _boundings = boundings;
605 recount();
606 }
607
recount(bool resetScroll)608 void FieldAutocomplete::recount(bool resetScroll) {
609 int32 h = 0, oldst = _scroll->scrollTop(), st = oldst, maxh = 4.5 * st::mentionHeight;
610 if (!_srows.empty()) {
611 int32 stickersPerRow = qMax(1, int32(_boundings.width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));
612 int32 rows = rowscount(_srows.size(), stickersPerRow);
613 h = st::stickerPanPadding + rows * st::stickerPanSize.height();
614 } else if (!_mrows.empty()) {
615 h = _mrows.size() * st::mentionHeight;
616 } else if (!_hrows.empty()) {
617 h = _hrows.size() * st::mentionHeight;
618 } else if (!_brows.empty()) {
619 h = _brows.size() * st::mentionHeight;
620 }
621
622 if (_inner->width() != _boundings.width() || _inner->height() != h) {
623 _inner->resize(_boundings.width(), h);
624 }
625 if (h > _boundings.height()) h = _boundings.height();
626 if (h > maxh) h = maxh;
627 if (width() != _boundings.width() || height() != h) {
628 setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h);
629 _scroll->resize(_boundings.width(), h);
630 } else if (y() != _boundings.y() + _boundings.height() - h) {
631 move(_boundings.x(), _boundings.y() + _boundings.height() - h);
632 }
633 if (resetScroll) st = 0;
634 if (st != oldst) _scroll->scrollToY(st);
635 if (resetScroll) _inner->clearSel();
636 }
637
hideFast()638 void FieldAutocomplete::hideFast() {
639 _a_opacity.stop();
640 hideFinish();
641 }
642
hideAnimated()643 void FieldAutocomplete::hideAnimated() {
644 if (isHidden() || _hiding) {
645 return;
646 }
647
648 if (_cache.isNull()) {
649 _scroll->show();
650 _cache = Ui::GrabWidget(this);
651 }
652 _scroll->hide();
653 _hiding = true;
654 _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration);
655 setAttribute(Qt::WA_OpaquePaintEvent, false);
656 }
657
hideFinish()658 void FieldAutocomplete::hideFinish() {
659 hide();
660 _hiding = false;
661 _filter = qsl("-");
662 _inner->clearSel(true);
663 }
664
showAnimated()665 void FieldAutocomplete::showAnimated() {
666 if (!isHidden() && !_hiding) {
667 return;
668 }
669 if (_cache.isNull()) {
670 _stickersSeed = base::RandomValue<uint64>();
671 _scroll->show();
672 _cache = Ui::GrabWidget(this);
673 }
674 _scroll->hide();
675 _hiding = false;
676 show();
677 _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration);
678 setAttribute(Qt::WA_OpaquePaintEvent, false);
679 }
680
animationCallback()681 void FieldAutocomplete::animationCallback() {
682 update();
683 if (!_a_opacity.animating()) {
684 _cache = QPixmap();
685 setAttribute(Qt::WA_OpaquePaintEvent);
686 if (_hiding) {
687 hideFinish();
688 } else {
689 _scroll->show();
690 _inner->clearSel();
691 }
692 }
693 }
694
filter() const695 const QString &FieldAutocomplete::filter() const {
696 return _filter;
697 }
698
chat() const699 ChatData *FieldAutocomplete::chat() const {
700 return _chat;
701 }
702
channel() const703 ChannelData *FieldAutocomplete::channel() const {
704 return _channel;
705 }
706
user() const707 UserData *FieldAutocomplete::user() const {
708 return _user;
709 }
710
innerTop()711 int32 FieldAutocomplete::innerTop() {
712 return _scroll->scrollTop();
713 }
714
innerBottom()715 int32 FieldAutocomplete::innerBottom() {
716 return _scroll->scrollTop() + _scroll->height();
717 }
718
chooseSelected(ChooseMethod method) const719 bool FieldAutocomplete::chooseSelected(ChooseMethod method) const {
720 return _inner->chooseSelected(method);
721 }
722
setSendMenuType(Fn<SendMenu::Type ()> && callback)723 void FieldAutocomplete::setSendMenuType(Fn<SendMenu::Type()> &&callback) {
724 _inner->setSendMenuType(std::move(callback));
725 }
726
eventFilter(QObject * obj,QEvent * e)727 bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
728 auto hidden = isHidden();
729 auto moderate = Core::App().settings().moderateModeEnabled();
730 if (hidden && !moderate) return QWidget::eventFilter(obj, e);
731
732 if (e->type() == QEvent::KeyPress) {
733 QKeyEvent *ev = static_cast<QKeyEvent*>(e);
734 if (!(ev->modifiers() & (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier | Qt::MetaModifier))) {
735 const auto key = ev->key();
736 if (!hidden) {
737 if (key == Qt::Key_Up || key == Qt::Key_Down || (!_srows.empty() && (key == Qt::Key_Left || key == Qt::Key_Right))) {
738 return _inner->moveSel(key);
739 } else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
740 return _inner->chooseSelected(ChooseMethod::ByEnter);
741 }
742 }
743 if (moderate
744 && ((key >= Qt::Key_1 && key <= Qt::Key_9)
745 || key == Qt::Key_Q
746 || key == Qt::Key_W)) {
747
748 return _moderateKeyActivateCallback
749 ? _moderateKeyActivateCallback(key)
750 : false;
751 }
752 }
753 }
754 return QWidget::eventFilter(obj, e);
755 }
756
Inner(not_null<Window::SessionController * > controller,not_null<FieldAutocomplete * > parent,not_null<MentionRows * > mrows,not_null<HashtagRows * > hrows,not_null<BotCommandRows * > brows,not_null<StickerRows * > srows)757 FieldAutocomplete::Inner::Inner(
758 not_null<Window::SessionController*> controller,
759 not_null<FieldAutocomplete*> parent,
760 not_null<MentionRows*> mrows,
761 not_null<HashtagRows*> hrows,
762 not_null<BotCommandRows*> brows,
763 not_null<StickerRows*> srows)
764 : _controller(controller)
765 , _parent(parent)
766 , _mrows(mrows)
767 , _hrows(hrows)
768 , _brows(brows)
769 , _srows(srows)
770 , _pathGradient(std::make_unique<Ui::PathShiftGradient>(
771 st::windowBgRipple,
772 st::windowBgOver,
773 [=] { update(); }))
__anonc65188840e02null774 , _previewTimer([=] { showPreview(); }) {
775 controller->session().downloaderTaskFinished(
__anonc65188840f02null776 ) | rpl::start_with_next([=] {
777 update();
778 }, lifetime());
779
780 controller->adaptive().value(
__anonc65188841002null781 ) | rpl::start_with_next([=] {
782 _isOneColumn = controller->adaptive().isOneColumn();
783 update();
784 }, lifetime());
785 }
786
paintEvent(QPaintEvent * e)787 void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
788 Painter p(this);
789
790 QRect r(e->rect());
791 if (r != rect()) p.setClipRect(r);
792
793 auto mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize;
794 auto mentionwidth = width()
795 - mentionleft
796 - 2 * st::mentionPadding.right();
797 auto htagleft = st::historyAttach.width
798 + st::historyComposeField.textMargins.left()
799 - st::lineWidth;
800 auto htagwidth = width()
801 - st::mentionPadding.right()
802 - htagleft
803 - st::defaultScrollArea.width;
804
805 if (!_srows->empty()) {
806 _pathGradient->startFrame(
807 0,
808 width(),
809 std::min(st::msgMaxWidth / 2, width() / 2));
810
811 int32 rows = rowscount(_srows->size(), _stickersPerRow);
812 int32 fromrow = floorclamp(r.y() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows);
813 int32 torow = ceilclamp(r.y() + r.height() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows);
814 int32 fromcol = floorclamp(r.x() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow);
815 int32 tocol = ceilclamp(r.x() + r.width() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow);
816 for (int32 row = fromrow; row < torow; ++row) {
817 for (int32 col = fromcol; col < tocol; ++col) {
818 int32 index = row * _stickersPerRow + col;
819 if (index >= _srows->size()) break;
820
821 auto &sticker = (*_srows)[index];
822 const auto document = sticker.document;
823 const auto &media = sticker.documentMedia;
824 if (!document->sticker()) continue;
825
826 if (document->sticker()->animated
827 && !sticker.animated
828 && media->loaded()) {
829 setupLottie(sticker);
830 }
831
832 QPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height());
833 if (_sel == index) {
834 QPoint tl(pos);
835 if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width());
836 Ui::FillRoundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, Ui::StickerHoverCorners);
837 }
838
839 media->checkStickerSmall();
840 auto w = 1;
841 auto h = 1;
842 if (sticker.animated && !document->dimensions.isEmpty()) {
843 const auto request = Lottie::FrameRequest{ stickerBoundingBox() * cIntRetinaFactor() };
844 const auto size = request.size(document->dimensions, true) / cIntRetinaFactor();
845 w = std::max(size.width(), 1);
846 h = std::max(size.height(), 1);
847 } else {
848 const auto coef = std::min(
849 std::min(
850 (st::stickerPanSize.width() - st::roundRadiusSmall * 2) / float64(document->dimensions.width()),
851 (st::stickerPanSize.height() - st::roundRadiusSmall * 2) / float64(document->dimensions.height())),
852 1.);
853 w = std::max(qRound(coef * document->dimensions.width()), 1);
854 h = std::max(qRound(coef * document->dimensions.height()), 1);
855 }
856
857 if (sticker.animated && sticker.animated->ready()) {
858 const auto frame = sticker.animated->frame();
859 const auto size = frame.size() / cIntRetinaFactor();
860 const auto ppos = pos + QPoint(
861 (st::stickerPanSize.width() - size.width()) / 2,
862 (st::stickerPanSize.height() - size.height()) / 2);
863 p.drawImage(
864 QRect(ppos, size),
865 frame);
866 const auto paused = _controller->isGifPausedAtLeastFor(
867 Window::GifPauseReason::SavedGifs);
868 if (!paused) {
869 sticker.animated->markFrameShown();
870 }
871 } else if (const auto image = media->getStickerSmall()) {
872 QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
873 p.drawPixmapLeft(ppos, width(), image->pix(w, h));
874 } else {
875 QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
876 ChatHelpers::PaintStickerThumbnailPath(
877 p,
878 media.get(),
879 QRect(ppos, QSize(w, h)),
880 _pathGradient.get());
881 }
882 }
883 }
884 } else {
885 int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1;
886 int32 last = !_mrows->empty()
887 ? _mrows->size()
888 : !_hrows->empty()
889 ? _hrows->size()
890 : _brows->size();
891 auto filter = _parent->filter();
892 bool hasUsername = filter.indexOf('@') > 0;
893 int filterSize = filter.size();
894 bool filterIsEmpty = filter.isEmpty();
895 for (int32 i = from; i < to; ++i) {
896 if (i >= last) break;
897
898 bool selected = (i == _sel);
899 if (selected) {
900 p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver);
901 int skip = (st::mentionHeight - st::smallCloseIconOver.height()) / 2;
902 if (!_hrows->empty() || (!_mrows->empty() && i < _recentInlineBotsInRows)) {
903 st::smallCloseIconOver.paint(p, QPoint(width() - st::smallCloseIconOver.width() - skip, i * st::mentionHeight + skip), width());
904 }
905 }
906 if (!_mrows->empty()) {
907 auto &row = _mrows->at(i);
908 const auto user = row.user;
909 auto first = (!filterIsEmpty && user->username.startsWith(filter, Qt::CaseInsensitive)) ? ('@' + user->username.mid(0, filterSize)) : QString();
910 auto second = first.isEmpty() ? (user->username.isEmpty() ? QString() : ('@' + user->username)) : user->username.mid(filterSize);
911 auto firstwidth = st::mentionFont->width(first);
912 auto secondwidth = st::mentionFont->width(second);
913 auto unamewidth = firstwidth + secondwidth;
914 auto namewidth = user->nameText().maxWidth();
915 if (mentionwidth < unamewidth + namewidth) {
916 namewidth = (mentionwidth * namewidth) / (namewidth + unamewidth);
917 unamewidth = mentionwidth - namewidth;
918 if (firstwidth < unamewidth + st::mentionFont->elidew) {
919 if (firstwidth < unamewidth) {
920 first = st::mentionFont->elided(first, unamewidth);
921 } else if (!second.isEmpty()) {
922 first = st::mentionFont->elided(first + second, unamewidth);
923 second = QString();
924 }
925 } else {
926 second = st::mentionFont->elided(second, unamewidth - firstwidth);
927 }
928 }
929 user->loadUserpic();
930 user->paintUserpicLeft(p, row.userpic, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width(), st::mentionPhotoSize);
931
932 p.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);
933 user->nameText().drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth);
934
935 p.setFont(st::mentionFont);
936 p.setPen(selected ? st::mentionFgOverActive : st::mentionFgActive);
937 p.drawText(mentionleft + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first);
938 if (!second.isEmpty()) {
939 p.setPen(selected ? st::mentionFgOver : st::mentionFg);
940 p.drawText(mentionleft + namewidth + st::mentionPadding.right() + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second);
941 }
942 } else if (!_hrows->empty()) {
943 QString hrow = _hrows->at(i);
944 QString first = filterIsEmpty ? QString() : ('#' + hrow.mid(0, filterSize));
945 QString second = filterIsEmpty ? ('#' + hrow) : hrow.mid(filterSize);
946 int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second);
947 if (htagwidth < firstwidth + secondwidth) {
948 if (htagwidth < firstwidth + st::mentionFont->elidew) {
949 first = st::mentionFont->elided(first + second, htagwidth);
950 second = QString();
951 } else {
952 second = st::mentionFont->elided(second, htagwidth - firstwidth);
953 }
954 }
955
956 p.setFont(st::mentionFont);
957 if (!first.isEmpty()) {
958 p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p);
959 p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first);
960 }
961 if (!second.isEmpty()) {
962 p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p);
963 p.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second);
964 }
965 } else {
966 auto &row = _brows->at(i);
967 const auto user = row.user;
968
969 auto toHighlight = row.command;
970 int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1);
971 if (hasUsername || botStatus == 0 || botStatus == 2) {
972 toHighlight += '@' + user->username;
973 }
974 user->loadUserpic();
975 user->paintUserpicLeft(p, row.userpic, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width(), st::mentionPhotoSize);
976
977 auto commandText = '/' + toHighlight;
978
979 p.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);
980 p.setFont(st::semiboldFont);
981 p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop + st::semiboldFont->ascent, commandText);
982
983 auto commandTextWidth = st::semiboldFont->width(commandText);
984 auto addleft = commandTextWidth + st::mentionPadding.left();
985 auto widthleft = mentionwidth - addleft;
986
987 if (!row.description.isEmpty()
988 && row.descriptionText.isEmpty()) {
989 row.descriptionText.setText(
990 st::defaultTextStyle,
991 row.description,
992 Ui::NameTextOptions());
993 }
994 if (widthleft > st::mentionFont->elidew && !row.descriptionText.isEmpty()) {
995 p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p);
996 row.descriptionText.drawElided(p, mentionleft + addleft, i * st::mentionHeight + st::mentionTop, widthleft);
997 }
998 }
999 }
1000 p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg);
1001 }
1002 p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerTop(), width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg);
1003 }
1004
resizeEvent(QResizeEvent * e)1005 void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {
1006 _stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));
1007 }
1008
mouseMoveEvent(QMouseEvent * e)1009 void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
1010 const auto globalPosition = e->globalPos();
1011 if (!_lastMousePosition) {
1012 _lastMousePosition = globalPosition;
1013 return;
1014 } else if (!_mouseSelection
1015 && *_lastMousePosition == globalPosition) {
1016 return;
1017 }
1018 selectByMouse(globalPosition);
1019 }
1020
clearSel(bool hidden)1021 void FieldAutocomplete::Inner::clearSel(bool hidden) {
1022 _overDelete = false;
1023 _mouseSelection = false;
1024 _lastMousePosition = std::nullopt;
1025 setSel((_mrows->empty() && _brows->empty() && _hrows->empty()) ? -1 : 0);
1026 if (hidden) {
1027 _down = -1;
1028 _previewShown = false;
1029 }
1030 }
1031
moveSel(int key)1032 bool FieldAutocomplete::Inner::moveSel(int key) {
1033 _mouseSelection = false;
1034 _lastMousePosition = std::nullopt;
1035
1036 int32 maxSel = !_mrows->empty()
1037 ? _mrows->size()
1038 : !_hrows->empty()
1039 ? _hrows->size()
1040 : !_brows->empty()
1041 ? _brows->size()
1042 : _srows->size();
1043 int32 direction = (key == Qt::Key_Up) ? -1 : (key == Qt::Key_Down ? 1 : 0);
1044 if (!_srows->empty()) {
1045 if (key == Qt::Key_Left) {
1046 direction = -1;
1047 } else if (key == Qt::Key_Right) {
1048 direction = 1;
1049 } else {
1050 direction *= _stickersPerRow;
1051 }
1052 }
1053 if (_sel >= maxSel || _sel < 0) {
1054 if (direction < -1) {
1055 setSel(((maxSel - 1) / _stickersPerRow) * _stickersPerRow, true);
1056 } else if (direction < 0) {
1057 setSel(maxSel - 1, true);
1058 } else {
1059 setSel(0, true);
1060 }
1061 return (_sel >= 0 && _sel < maxSel);
1062 }
1063 setSel((_sel + direction >= maxSel || _sel + direction < 0) ? -1 : (_sel + direction), true);
1064 return true;
1065 }
1066
chooseSelected(FieldAutocomplete::ChooseMethod method) const1067 bool FieldAutocomplete::Inner::chooseSelected(
1068 FieldAutocomplete::ChooseMethod method) const {
1069 return chooseAtIndex(method, _sel);
1070 }
1071
chooseAtIndex(FieldAutocomplete::ChooseMethod method,int index,Api::SendOptions options) const1072 bool FieldAutocomplete::Inner::chooseAtIndex(
1073 FieldAutocomplete::ChooseMethod method,
1074 int index,
1075 Api::SendOptions options) const {
1076 if (index < 0 || (method == ChooseMethod::ByEnter && _mouseSelection)) {
1077 return false;
1078 }
1079 if (!_srows->empty()) {
1080 if (index < _srows->size()) {
1081 const auto document = (*_srows)[index].document;
1082 _stickerChosen.fire({ document, options, method });
1083 return true;
1084 }
1085 } else if (!_mrows->empty()) {
1086 if (index < _mrows->size()) {
1087 _mentionChosen.fire({ _mrows->at(index).user, method });
1088 return true;
1089 }
1090 } else if (!_hrows->empty()) {
1091 if (index < _hrows->size()) {
1092 _hashtagChosen.fire({ '#' + _hrows->at(index), method });
1093 return true;
1094 }
1095 } else if (!_brows->empty()) {
1096 if (index < _brows->size()) {
1097 const auto user = _brows->at(index).user;
1098 const auto &command = _brows->at(index).command;
1099 const auto botStatus = _parent->chat()
1100 ? _parent->chat()->botStatus
1101 : ((_parent->channel() && _parent->channel()->isMegagroup())
1102 ? _parent->channel()->mgInfo->botStatus
1103 : -1);
1104
1105 const auto insertUsername = (botStatus == 0
1106 || botStatus == 2
1107 || _parent->filter().indexOf('@') > 0);
1108 const auto commandString = QString("/%1%2").arg(
1109 command,
1110 insertUsername ? ('@' + user->username) : QString());
1111
1112 _botCommandChosen.fire({ commandString, method });
1113 return true;
1114 }
1115 }
1116 return false;
1117 }
1118
setRecentInlineBotsInRows(int32 bots)1119 void FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) {
1120 _recentInlineBotsInRows = bots;
1121 }
1122
mousePressEvent(QMouseEvent * e)1123 void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
1124 selectByMouse(e->globalPos());
1125 if (e->button() == Qt::LeftButton) {
1126 if (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) {
1127 bool removed = false;
1128 if (_mrows->empty()) {
1129 QString toRemove = _hrows->at(_sel);
1130 RecentHashtagPack &recent(cRefRecentWriteHashtags());
1131 for (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) {
1132 if (i->first == toRemove) {
1133 i = recent.erase(i);
1134 removed = true;
1135 } else {
1136 ++i;
1137 }
1138 }
1139 } else {
1140 UserData *toRemove = _mrows->at(_sel).user;
1141 RecentInlineBots &recent(cRefRecentInlineBots());
1142 int32 index = recent.indexOf(toRemove);
1143 if (index >= 0) {
1144 recent.remove(index);
1145 removed = true;
1146 }
1147 }
1148 if (removed) {
1149 _controller->session().local().writeRecentHashtagsAndBots();
1150 }
1151 _parent->updateFiltered();
1152
1153 selectByMouse(e->globalPos());
1154 } else if (_srows->empty()) {
1155 chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
1156 } else {
1157 _down = _sel;
1158 _previewTimer.callOnce(QApplication::startDragTime());
1159 }
1160 }
1161 }
1162
mouseReleaseEvent(QMouseEvent * e)1163 void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {
1164 _previewTimer.cancel();
1165
1166 int32 pressed = _down;
1167 _down = -1;
1168
1169 selectByMouse(e->globalPos());
1170
1171 if (_previewShown) {
1172 _previewShown = false;
1173 return;
1174 }
1175
1176 if (_sel < 0 || _sel != pressed || _srows->empty()) return;
1177
1178 chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
1179 }
1180
contextMenuEvent(QContextMenuEvent * e)1181 void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
1182 if (_sel < 0 || _srows->empty() || _down >= 0) {
1183 return;
1184 }
1185 const auto index = _sel;
1186 const auto type = _sendMenuType
1187 ? _sendMenuType()
1188 : SendMenu::Type::Disabled;
1189 const auto method = FieldAutocomplete::ChooseMethod::ByClick;
1190 _menu = base::make_unique_q<Ui::PopupMenu>(this);
1191
1192 const auto send = [=](Api::SendOptions options) {
1193 chooseAtIndex(method, index, options);
1194 };
1195 SendMenu::FillSendMenu(
1196 _menu,
1197 type,
1198 SendMenu::DefaultSilentCallback(send),
1199 SendMenu::DefaultScheduleCallback(this, type, send));
1200
1201 if (!_menu->empty()) {
1202 _menu->popup(QCursor::pos());
1203 }
1204 }
1205
enterEventHook(QEnterEvent * e)1206 void FieldAutocomplete::Inner::enterEventHook(QEnterEvent *e) {
1207 setMouseTracking(true);
1208 }
1209
leaveEventHook(QEvent * e)1210 void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {
1211 setMouseTracking(false);
1212 if (_mouseSelection) {
1213 setSel(-1);
1214 _mouseSelection = false;
1215 _lastMousePosition = std::nullopt;
1216 }
1217 }
1218
updateSelectedRow()1219 void FieldAutocomplete::Inner::updateSelectedRow() {
1220 if (_sel >= 0) {
1221 if (_srows->empty()) {
1222 update(0, _sel * st::mentionHeight, width(), st::mentionHeight);
1223 } else {
1224 int32 row = _sel / _stickersPerRow, col = _sel % _stickersPerRow;
1225 update(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height(), st::stickerPanSize.width(), st::stickerPanSize.height());
1226 }
1227 }
1228 }
1229
setSel(int sel,bool scroll)1230 void FieldAutocomplete::Inner::setSel(int sel, bool scroll) {
1231 updateSelectedRow();
1232 _sel = sel;
1233 updateSelectedRow();
1234
1235 if (scroll && _sel >= 0) {
1236 if (_srows->empty()) {
1237 _scrollToRequested.fire({
1238 _sel * st::mentionHeight,
1239 (_sel + 1) * st::mentionHeight });
1240 } else {
1241 int32 row = _sel / _stickersPerRow;
1242 const auto padding = st::stickerPanPadding;
1243 _scrollToRequested.fire({
1244 padding + row * st::stickerPanSize.height(),
1245 padding + (row + 1) * st::stickerPanSize.height() });
1246 }
1247 }
1248 }
1249
rowsUpdated()1250 void FieldAutocomplete::Inner::rowsUpdated() {
1251 if (_srows->empty()) {
1252 _stickersLifetime.destroy();
1253 }
1254 }
1255
getLottieRenderer()1256 auto FieldAutocomplete::Inner::getLottieRenderer()
1257 -> std::shared_ptr<Lottie::FrameRenderer> {
1258 if (auto result = _lottieRenderer.lock()) {
1259 return result;
1260 }
1261 auto result = Lottie::MakeFrameRenderer();
1262 _lottieRenderer = result;
1263 return result;
1264 }
1265
setupLottie(StickerSuggestion & suggestion)1266 void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
1267 const auto document = suggestion.document;
1268 suggestion.animated = ChatHelpers::LottiePlayerFromDocument(
1269 suggestion.documentMedia.get(),
1270 ChatHelpers::StickerLottieSize::InlineResults,
1271 stickerBoundingBox() * cIntRetinaFactor(),
1272 Lottie::Quality::Default,
1273 getLottieRenderer());
1274
1275 suggestion.animated->updates(
1276 ) | rpl::start_with_next([=] {
1277 repaintSticker(document);
1278 }, _stickersLifetime);
1279 }
1280
stickerBoundingBox() const1281 QSize FieldAutocomplete::Inner::stickerBoundingBox() const {
1282 return QSize(
1283 st::stickerPanSize.width() - st::roundRadiusSmall * 2,
1284 st::stickerPanSize.height() - st::roundRadiusSmall * 2);
1285 }
1286
repaintSticker(not_null<DocumentData * > document)1287 void FieldAutocomplete::Inner::repaintSticker(
1288 not_null<DocumentData*> document) {
1289 const auto i = ranges::find(
1290 *_srows,
1291 document,
1292 &StickerSuggestion::document);
1293 if (i == end(*_srows)) {
1294 return;
1295 }
1296 const auto index = (i - begin(*_srows));
1297 const auto row = (index / _stickersPerRow);
1298 const auto col = (index % _stickersPerRow);
1299 update(
1300 st::stickerPanPadding + col * st::stickerPanSize.width(),
1301 st::stickerPanPadding + row * st::stickerPanSize.height(),
1302 st::stickerPanSize.width(),
1303 st::stickerPanSize.height());
1304 }
1305
selectByMouse(QPoint globalPosition)1306 void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {
1307 _mouseSelection = true;
1308 _lastMousePosition = globalPosition;
1309 const auto mouse = mapFromGlobal(globalPosition);
1310
1311 if (_down >= 0 && !_previewShown) {
1312 return;
1313 }
1314
1315 int32 sel = -1, maxSel = 0;
1316 if (!_srows->empty()) {
1317 int32 row = (mouse.y() >= st::stickerPanPadding) ? ((mouse.y() - st::stickerPanPadding) / st::stickerPanSize.height()) : -1;
1318 int32 col = (mouse.x() >= st::stickerPanPadding) ? ((mouse.x() - st::stickerPanPadding) / st::stickerPanSize.width()) : -1;
1319 if (row >= 0 && col >= 0) {
1320 sel = row * _stickersPerRow + col;
1321 }
1322 maxSel = _srows->size();
1323 _overDelete = false;
1324 } else {
1325 sel = mouse.y() / int32(st::mentionHeight);
1326 maxSel = !_mrows->empty()
1327 ? _mrows->size()
1328 : !_hrows->empty()
1329 ? _hrows->size()
1330 : _brows->size();
1331 _overDelete = (!_hrows->empty() || (!_mrows->empty() && sel < _recentInlineBotsInRows)) ? (mouse.x() >= width() - st::mentionHeight) : false;
1332 }
1333 if (sel < 0 || sel >= maxSel) {
1334 sel = -1;
1335 }
1336 if (sel != _sel) {
1337 setSel(sel);
1338 if (_down >= 0 && _sel >= 0 && _down != _sel) {
1339 _down = _sel;
1340 if (_down >= 0 && _down < _srows->size()) {
1341 _controller->widget()->showMediaPreview(
1342 (*_srows)[_down].document->stickerSetOrigin(),
1343 (*_srows)[_down].document);
1344 }
1345 }
1346 }
1347 }
1348
onParentGeometryChanged()1349 void FieldAutocomplete::Inner::onParentGeometryChanged() {
1350 const auto globalPosition = QCursor::pos();
1351 if (rect().contains(mapFromGlobal(globalPosition))) {
1352 setMouseTracking(true);
1353 if (_mouseSelection) {
1354 selectByMouse(globalPosition);
1355 }
1356 }
1357 }
1358
showPreview()1359 void FieldAutocomplete::Inner::showPreview() {
1360 if (_down >= 0 && _down < _srows->size()) {
1361 _controller->widget()->showMediaPreview(
1362 (*_srows)[_down].document->stickerSetOrigin(),
1363 (*_srows)[_down].document);
1364 _previewShown = true;
1365 }
1366 }
1367
setSendMenuType(Fn<SendMenu::Type ()> && callback)1368 void FieldAutocomplete::Inner::setSendMenuType(
1369 Fn<SendMenu::Type()> &&callback) {
1370 _sendMenuType = std::move(callback);
1371 }
1372
mentionChosen() const1373 auto FieldAutocomplete::Inner::mentionChosen() const
1374 -> rpl::producer<FieldAutocomplete::MentionChosen> {
1375 return _mentionChosen.events();
1376 }
1377
hashtagChosen() const1378 auto FieldAutocomplete::Inner::hashtagChosen() const
1379 -> rpl::producer<FieldAutocomplete::HashtagChosen> {
1380 return _hashtagChosen.events();
1381 }
1382
botCommandChosen() const1383 auto FieldAutocomplete::Inner::botCommandChosen() const
1384 -> rpl::producer<FieldAutocomplete::BotCommandChosen> {
1385 return _botCommandChosen.events();
1386 }
1387
stickerChosen() const1388 auto FieldAutocomplete::Inner::stickerChosen() const
1389 -> rpl::producer<FieldAutocomplete::StickerChosen> {
1390 return _stickerChosen.events();
1391 }
1392
scrollToRequested() const1393 auto FieldAutocomplete::Inner::scrollToRequested() const
1394 -> rpl::producer<ScrollTo> {
1395 return _scrollToRequested.events();
1396 }
1397