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