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 "boxes/language_box.h"
9 
10 #include "lang/lang_keys.h"
11 #include "ui/widgets/checkbox.h"
12 #include "ui/widgets/buttons.h"
13 #include "ui/widgets/labels.h"
14 #include "ui/widgets/multi_select.h"
15 #include "ui/widgets/scroll_area.h"
16 #include "ui/widgets/dropdown_menu.h"
17 #include "ui/widgets/box_content_divider.h"
18 #include "ui/text/text_entity.h"
19 #include "ui/wrap/vertical_layout.h"
20 #include "ui/wrap/slide_wrap.h"
21 #include "ui/effects/ripple_animation.h"
22 #include "ui/toast/toast.h"
23 #include "ui/text/text_options.h"
24 #include "storage/localstorage.h"
25 #include "ui/boxes/confirm_box.h"
26 #include "mainwidget.h"
27 #include "mainwindow.h"
28 #include "core/application.h"
29 #include "lang/lang_instance.h"
30 #include "lang/lang_cloud_manager.h"
31 #include "styles/style_layers.h"
32 #include "styles/style_boxes.h"
33 #include "styles/style_info.h"
34 #include "styles/style_passport.h"
35 #include "styles/style_chat_helpers.h"
36 
37 #include <QtGui/QGuiApplication>
38 #include <QtGui/QClipboard>
39 
40 namespace {
41 
42 using Language = Lang::Language;
43 using Languages = Lang::CloudManager::Languages;
44 
45 class Rows : public Ui::RpWidget {
46 public:
47 	Rows(
48 		QWidget *parent,
49 		const Languages &data,
50 		const QString &chosen,
51 		bool areOfficial);
52 
53 	void filter(const QString &query);
54 
55 	int count() const;
56 	int selected() const;
57 	void setSelected(int selected);
58 	rpl::producer<bool> hasSelection() const;
59 	rpl::producer<bool> isEmpty() const;
60 
61 	void activateSelected();
62 	rpl::producer<Language> activations() const;
63 	void changeChosen(const QString &chosen);
64 
65 	Ui::ScrollToRequest rowScrollRequest(int index) const;
66 
67 	static int DefaultRowHeight();
68 
69 protected:
70 	int resizeGetHeight(int newWidth) override;
71 
72 	void paintEvent(QPaintEvent *e) override;
73 	void mouseMoveEvent(QMouseEvent *e) override;
74 	void mousePressEvent(QMouseEvent *e) override;
75 	void mouseReleaseEvent(QMouseEvent *e) override;
76 	void leaveEventHook(QEvent *e) override;
77 
78 private:
79 	struct Row {
80 		Language data;
81 		Ui::Text::String title = { st::boxWideWidth / 2 };
82 		Ui::Text::String description = { st::boxWideWidth / 2 };
83 		int top = 0;
84 		int height = 0;
85 		mutable std::unique_ptr<Ui::RippleAnimation> ripple;
86 		mutable std::unique_ptr<Ui::RippleAnimation> menuToggleRipple;
87 		bool menuToggleForceRippled = false;
88 		int titleHeight = 0;
89 		int descriptionHeight = 0;
90 		QStringList keywords;
91 		std::unique_ptr<Ui::RadioView> check;
92 		bool removed = false;
93 	};
94 	struct RowSelection {
95 		int index = 0;
96 
operator ==__anon9f7bb7ea0111::Rows::RowSelection97 		inline bool operator==(const RowSelection &other) const {
98 			return (index == other.index);
99 		}
100 	};
101 	struct MenuSelection {
102 		int index = 0;
103 
operator ==__anon9f7bb7ea0111::Rows::MenuSelection104 		inline bool operator==(const MenuSelection &other) const {
105 			return (index == other.index);
106 		}
107 	};
108 	using Selection = std::variant<v::null_t, RowSelection, MenuSelection>;
109 
110 	void updateSelected(Selection selected);
111 	void updatePressed(Selection pressed);
112 	Rows::Row &rowByIndex(int index);
113 	const Rows::Row &rowByIndex(int index) const;
114 	Rows::Row &rowBySelection(Selection selected);
115 	const Rows::Row &rowBySelection(Selection selected) const;
116 	std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(
117 		Selection selected);
118 	[[maybe_unused]] const std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(
119 		Selection selected) const;
120 	std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(
121 		not_null<Row*> row,
122 		Selection selected);
123 	[[maybe_unused]] const std::unique_ptr<Ui::RippleAnimation> &rippleBySelection(
124 		not_null<const Row*> row,
125 		Selection selected) const;
126 	void addRipple(Selection selected, QPoint position);
127 	void ensureRippleBySelection(Selection selected);
128 	void ensureRippleBySelection(not_null<Row*> row, Selection selected);
129 	int indexFromSelection(Selection selected) const;
130 	int countAvailableWidth() const;
131 	int countAvailableWidth(int newWidth) const;
132 	QRect menuToggleArea() const;
133 	QRect menuToggleArea(not_null<const Row*> row) const;
134 	void repaint(Selection selected);
135 	void repaint(int index);
136 	void repaint(const Row &row);
137 	void repaintChecked(not_null<const Row*> row);
138 	void activateByIndex(int index);
139 
140 	void showMenu(int index);
141 	void setForceRippled(not_null<Row*> row, bool rippled);
142 	bool canShare(not_null<const Row*> row) const;
143 	bool canRemove(not_null<const Row*> row) const;
144 	bool hasMenu(not_null<const Row*> row) const;
145 	void share(not_null<const Row*> row) const;
146 	void remove(not_null<Row*> row);
147 	void restore(not_null<Row*> row);
148 
149 	std::vector<Row> _rows;
150 	std::vector<not_null<Row*>> _filtered;
151 	Selection _selected;
152 	Selection _pressed;
153 	QString _chosen;
154 	QStringList _query;
155 
156 	bool _areOfficial = false;
157 	bool _mouseSelection = false;
158 	QPoint _globalMousePosition;
159 	base::unique_qptr<Ui::DropdownMenu> _menu;
160 	int _menuShownIndex = -1;
161 	bool _menuOtherEntered = false;
162 
163 	rpl::event_stream<bool> _hasSelection;
164 	rpl::event_stream<Language> _activations;
165 	rpl::event_stream<bool> _isEmpty;
166 
167 };
168 
169 class Content : public Ui::RpWidget {
170 public:
171 	Content(
172 		QWidget *parent,
173 		const Languages &recent,
174 		const Languages &official);
175 
176 	Ui::ScrollToRequest jump(int rows);
177 	void filter(const QString &query);
178 	rpl::producer<Language> activations() const;
179 	void changeChosen(const QString &chosen);
180 	void activateBySubmit();
181 
182 private:
183 	void setupContent(
184 		const Languages &recent,
185 		const Languages &official);
186 
187 	Fn<Ui::ScrollToRequest(int rows)> _jump;
188 	Fn<void(const QString &query)> _filter;
189 	Fn<rpl::producer<Language>()> _activations;
190 	Fn<void(const QString &chosen)> _changeChosen;
191 	Fn<void()> _activateBySubmit;
192 
193 };
194 
PrepareLists()195 std::pair<Languages, Languages> PrepareLists() {
196 	const auto projId = [](const Language &language) {
197 		return language.id;
198 	};
199 	const auto current = Lang::LanguageIdOrDefault(Lang::Id());
200 	auto official = Lang::CurrentCloudManager().languageList();
201 	auto recent = Local::readRecentLanguages();
202 	ranges::stable_partition(recent, [&](const Language &language) {
203 		return (language.id == current);
204 	});
205 	if (recent.empty() || recent.front().id != current) {
206 		if (ranges::find(official, current, projId) == end(official)) {
207 			const auto generate = [&] {
208 				const auto name = (current == "#custom")
209 					? "Custom lang pack"
210 					: Lang::GetInstance().name();
211 				return Language{
212 					current,
213 					QString(),
214 					QString(),
215 					name,
216 					Lang::GetInstance().nativeName()
217 				};
218 			};
219 			recent.insert(begin(recent), generate());
220 		}
221 	}
222 	auto i = begin(official), e = end(official);
223 	const auto remover = [&](const Language &language) {
224 		auto k = ranges::find(i, e, language.id, projId);
225 		if (k == e) {
226 			return false;
227 		}
228 		for (; k != i; --k) {
229 			std::swap(*k, *(k - 1));
230 		}
231 		++i;
232 		return true;
233 	};
234 	recent.erase(ranges::remove_if(recent, remover), end(recent));
235 	return { std::move(recent), std::move(official) };
236 }
237 
Rows(QWidget * parent,const Languages & data,const QString & chosen,bool areOfficial)238 Rows::Rows(
239 	QWidget *parent,
240 	const Languages &data,
241 	const QString &chosen,
242 	bool areOfficial)
243 : RpWidget(parent)
244 , _chosen(chosen)
245 , _areOfficial(areOfficial) {
246 	const auto descriptionOptions = TextParseOptions{
247 		TextParseMultiline,
248 		0,
249 		0,
250 		Qt::LayoutDirectionAuto
251 	};
252 	_rows.reserve(data.size());
253 	for (const auto &item : data) {
254 		_rows.push_back(Row{ item });
255 		auto &row = _rows.back();
256 		row.check = std::make_unique<Ui::RadioView>(
257 			st::langsRadio,
258 			(row.data.id == _chosen),
259 			[=, row = &row] { repaint(*row); });
260 		row.title.setText(
261 			st::semiboldTextStyle,
262 			item.nativeName,
263 			Ui::NameTextOptions());
264 		row.description.setText(
265 			st::defaultTextStyle,
266 			item.name,
267 			descriptionOptions);
268 		row.keywords = TextUtilities::PrepareSearchWords(
269 			item.name + ' ' + item.nativeName);
270 	}
271 	resizeToWidth(width());
272 	setAttribute(Qt::WA_MouseTracking);
273 	update();
274 }
275 
mouseMoveEvent(QMouseEvent * e)276 void Rows::mouseMoveEvent(QMouseEvent *e) {
277 	const auto position = e->globalPos();
278 	if (_menu) {
279 		const auto rect = (_menuShownIndex >= 0)
280 			? menuToggleArea(&rowByIndex(_menuShownIndex))
281 			: QRect();
282 		if (rect.contains(e->pos())) {
283 			if (!_menuOtherEntered) {
284 				_menuOtherEntered = true;
285 				_menu->otherEnter();
286 			}
287 		} else {
288 			if (_menuOtherEntered) {
289 				_menuOtherEntered = false;
290 				_menu->otherLeave();
291 			}
292 		}
293 	}
294 	if (!_mouseSelection && position == _globalMousePosition) {
295 		return;
296 	}
297 	_mouseSelection = true;
298 	_globalMousePosition = position;
299 	const auto index = [&] {
300 		const auto y = e->pos().y();
301 		if (y < 0) {
302 			return -1;
303 		}
304 		for (auto i = 0, till = count(); i != till; ++i) {
305 			const auto &row = rowByIndex(i);
306 			if (row.top + row.height > y) {
307 				return i;
308 			}
309 		}
310 		return -1;
311 	}();
312 	const auto row = (index >= 0) ? &rowByIndex(index) : nullptr;
313 	const auto inMenuToggle = (index >= 0 && hasMenu(row))
314 		? menuToggleArea(row).contains(e->pos())
315 		: false;
316 	if (index < 0) {
317 		updateSelected({});
318 	} else if (inMenuToggle) {
319 		updateSelected(MenuSelection{ index });
320 	} else if (!row->removed) {
321 		updateSelected(RowSelection{ index });
322 	} else {
323 		updateSelected({});
324 	}
325 }
326 
mousePressEvent(QMouseEvent * e)327 void Rows::mousePressEvent(QMouseEvent *e) {
328 	updatePressed(_selected);
329 	if (!v::is_null(_pressed)
330 		&& !rowBySelection(_pressed).menuToggleForceRippled) {
331 		addRipple(_pressed, e->pos());
332 	}
333 }
334 
menuToggleArea() const335 QRect Rows::menuToggleArea() const {
336 	const auto size = st::topBarSearch.width;
337 	const auto top = (DefaultRowHeight() - size) / 2;
338 	const auto skip = st::boxScroll.width
339 		- st::boxScroll.deltax
340 		+ top;
341 	const auto left = width() - skip - size;
342 	return QRect(left, top, size, size);
343 }
344 
menuToggleArea(not_null<const Row * > row) const345 QRect Rows::menuToggleArea(not_null<const Row*> row) const {
346 	return menuToggleArea().translated(0, row->top);
347 }
348 
addRipple(Selection selected,QPoint position)349 void Rows::addRipple(Selection selected, QPoint position) {
350 	Expects(!v::is_null(selected));
351 
352 	ensureRippleBySelection(selected);
353 
354 	const auto menu = v::is<MenuSelection>(selected);
355 	const auto &row = rowBySelection(selected);
356 	const auto menuArea = menuToggleArea(&row);
357 	auto &ripple = rippleBySelection(&row, selected);
358 	const auto topleft = menu ? menuArea.topLeft() : QPoint(0, row.top);
359 	ripple->add(position - topleft);
360 }
361 
ensureRippleBySelection(Selection selected)362 void Rows::ensureRippleBySelection(Selection selected) {
363 	ensureRippleBySelection(&rowBySelection(selected), selected);
364 }
365 
ensureRippleBySelection(not_null<Row * > row,Selection selected)366 void Rows::ensureRippleBySelection(not_null<Row*> row, Selection selected) {
367 	auto &ripple = rippleBySelection(row, selected);
368 	if (ripple) {
369 		return;
370 	}
371 	const auto menu = v::is<MenuSelection>(selected);
372 	const auto menuArea = menuToggleArea(row);
373 	auto mask = menu
374 		? Ui::RippleAnimation::ellipseMask(menuArea.size())
375 		: Ui::RippleAnimation::rectMask({ width(), row->height });
376 	ripple = std::make_unique<Ui::RippleAnimation>(
377 		st::defaultRippleAnimation,
378 		std::move(mask),
379 		[=] { repaintChecked(row); });
380 }
381 
mouseReleaseEvent(QMouseEvent * e)382 void Rows::mouseReleaseEvent(QMouseEvent *e) {
383 	if (_menu && e->button() == Qt::LeftButton) {
384 		if (_menu->isHiding()) {
385 			_menu->otherEnter();
386 		} else {
387 			_menu->otherLeave();
388 		}
389 	}
390 	const auto pressed = _pressed;
391 	updatePressed({});
392 	if (pressed == _selected) {
393 		v::match(pressed, [&](RowSelection data) {
394 			activateByIndex(data.index);
395 		}, [&](MenuSelection data) {
396 			showMenu(data.index);
397 		}, [](v::null_t) {});
398 	}
399 }
400 
canShare(not_null<const Row * > row) const401 bool Rows::canShare(not_null<const Row*> row) const {
402 	return !_areOfficial && !row->data.id.startsWith('#');
403 }
404 
canRemove(not_null<const Row * > row) const405 bool Rows::canRemove(not_null<const Row*> row) const {
406 	return !_areOfficial && !row->check->checked();
407 }
408 
hasMenu(not_null<const Row * > row) const409 bool Rows::hasMenu(not_null<const Row*> row) const {
410 	return canShare(row) || canRemove(row);
411 }
412 
share(not_null<const Row * > row) const413 void Rows::share(not_null<const Row*> row) const {
414 	const auto link = qsl("https://t.me/setlanguage/") + row->data.id;
415 	QGuiApplication::clipboard()->setText(link);
416 	Ui::Toast::Show(tr::lng_username_copied(tr::now));
417 }
418 
remove(not_null<Row * > row)419 void Rows::remove(not_null<Row*> row) {
420 	row->removed = true;
421 	Local::removeRecentLanguage(row->data.id);
422 }
423 
restore(not_null<Row * > row)424 void Rows::restore(not_null<Row*> row) {
425 	row->removed = false;
426 	Local::saveRecentLanguages(ranges::views::all(
427 		_rows
428 	) | ranges::views::filter([](const Row &row) {
429 		return !row.removed;
430 	}) | ranges::views::transform([](const Row &row) {
431 		return row.data;
432 	}) | ranges::to_vector);
433 }
434 
showMenu(int index)435 void Rows::showMenu(int index) {
436 	const auto row = &rowByIndex(index);
437 	if (_menu || !hasMenu(row)) {
438 		return;
439 	}
440 	_menu = base::make_unique_q<Ui::DropdownMenu>(window());
441 	const auto weak = _menu.get();
442 	_menu->setHiddenCallback([=] {
443 		weak->deleteLater();
444 		if (_menu == weak) {
445 			setForceRippled(row, false);
446 			_menuShownIndex = -1;
447 		}
448 	});
449 	_menu->setShowStartCallback([=] {
450 		if (_menu == weak) {
451 			setForceRippled(row, true);
452 			_menuShownIndex = index;
453 		}
454 	});
455 	_menu->setHideStartCallback([=] {
456 		if (_menu == weak) {
457 			setForceRippled(row, false);
458 			_menuShownIndex = -1;
459 		}
460 	});
461 	const auto addAction = [&](
462 			const QString &text,
463 			Fn<void()> callback) {
464 		return _menu->addAction(text, std::move(callback));
465 	};
466 	if (canShare(row)) {
467 		addAction(tr::lng_proxy_edit_share(tr::now), [=] { share(row); });
468 	}
469 	if (canRemove(row)) {
470 		if (row->removed) {
471 			addAction(tr::lng_proxy_menu_restore(tr::now), [=] {
472 				restore(row);
473 			});
474 		} else {
475 			addAction(tr::lng_proxy_menu_delete(tr::now), [=] {
476 				remove(row);
477 			});
478 		}
479 	}
480 	const auto toggle = menuToggleArea(row);
481 	const auto parentTopLeft = window()->mapToGlobal(QPoint());
482 	const auto buttonTopLeft = mapToGlobal(toggle.topLeft());
483 	const auto parent = QRect(parentTopLeft, window()->size());
484 	const auto button = QRect(buttonTopLeft, toggle.size());
485 	const auto bottom = button.y()
486 		+ st::proxyDropdownDownPosition.y()
487 		+ _menu->height()
488 		- parent.y();
489 	const auto top = button.y()
490 		+ st::proxyDropdownUpPosition.y()
491 		- _menu->height()
492 		- parent.y();
493 	_menuShownIndex = index;
494 	_menuOtherEntered = true;
495 	if (bottom > parent.height() && top >= 0) {
496 		const auto left = button.x()
497 			+ button.width()
498 			+ st::proxyDropdownUpPosition.x()
499 			- _menu->width()
500 			- parent.x();
501 		_menu->move(left, top);
502 		_menu->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
503 	} else {
504 		const auto left = button.x()
505 			+ button.width()
506 			+ st::proxyDropdownDownPosition.x()
507 			- _menu->width()
508 			- parent.x();
509 		_menu->move(left, bottom - _menu->height());
510 		_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
511 	}
512 }
513 
setForceRippled(not_null<Row * > row,bool rippled)514 void Rows::setForceRippled(not_null<Row*> row, bool rippled) {
515 	if (row->menuToggleForceRippled != rippled) {
516 		row->menuToggleForceRippled = rippled;
517 		auto &ripple = rippleBySelection(row, MenuSelection{});
518 		if (row->menuToggleForceRippled) {
519 			ensureRippleBySelection(row, MenuSelection{});
520 			if (ripple->empty()) {
521 				ripple->addFading();
522 			} else {
523 				ripple->lastUnstop();
524 			}
525 		} else {
526 			if (ripple) {
527 				ripple->lastStop();
528 			}
529 		}
530 	}
531 	repaint(*row);
532 }
533 
activateByIndex(int index)534 void Rows::activateByIndex(int index) {
535 	_activations.fire_copy(rowByIndex(index).data);
536 }
537 
leaveEventHook(QEvent * e)538 void Rows::leaveEventHook(QEvent *e) {
539 	updateSelected({});
540 	if (_menu && _menuOtherEntered) {
541 		_menuOtherEntered = false;
542 		_menu->otherLeave();
543 	}
544 }
545 
filter(const QString & query)546 void Rows::filter(const QString &query) {
547 	updateSelected({});
548 	updatePressed({});
549 	_menu = nullptr;
550 	_menuShownIndex = -1;
551 
552 	_query = TextUtilities::PrepareSearchWords(query);
553 
554 	const auto skip = [](
555 			const QStringList &haystack,
556 			const QStringList &needles) {
557 		const auto find = [](
558 				const QStringList &haystack,
559 				const QString &needle) {
560 			for (const auto &item : haystack) {
561 				if (item.startsWith(needle)) {
562 					return true;
563 				}
564 			}
565 			return false;
566 		};
567 		for (const auto &needle : needles) {
568 			if (!find(haystack, needle)) {
569 				return true;
570 			}
571 		}
572 		return false;
573 	};
574 
575 	if (!_query.isEmpty()) {
576 		_filtered.clear();
577 		_filtered.reserve(_rows.size());
578 		for (auto &row : _rows) {
579 			if (!skip(row.keywords, _query)) {
580 				_filtered.push_back(&row);
581 			} else {
582 				row.ripple = nullptr;
583 			}
584 		}
585 	}
586 
587 	resizeToWidth(width());
588 	Ui::SendPendingMoveResizeEvents(this);
589 
590 	_isEmpty.fire(count() == 0);
591 }
592 
count() const593 int Rows::count() const {
594 	return _query.isEmpty() ? _rows.size() : _filtered.size();
595 }
596 
indexFromSelection(Selection selected) const597 int Rows::indexFromSelection(Selection selected) const {
598 	return v::match(selected, [&](RowSelection data) {
599 		return data.index;
600 	}, [&](MenuSelection data) {
601 		return data.index;
602 	}, [](v::null_t) {
603 		return -1;
604 	});
605 }
606 
selected() const607 int Rows::selected() const {
608 	return indexFromSelection(_selected);
609 }
610 
activateSelected()611 void Rows::activateSelected() {
612 	const auto index = selected();
613 	if (index >= 0) {
614 		activateByIndex(index);
615 	}
616 }
617 
activations() const618 rpl::producer<Language> Rows::activations() const {
619 	return _activations.events();
620 }
621 
changeChosen(const QString & chosen)622 void Rows::changeChosen(const QString &chosen) {
623 	for (const auto &row : _rows) {
624 		row.check->setChecked(row.data.id == chosen, anim::type::normal);
625 	}
626 }
627 
setSelected(int selected)628 void Rows::setSelected(int selected) {
629 	_mouseSelection = false;
630 	const auto limit = count();
631 	if (selected >= 0 && selected < limit) {
632 		updateSelected(RowSelection{ selected });
633 	} else {
634 		updateSelected({});
635 	}
636 }
637 
hasSelection() const638 rpl::producer<bool> Rows::hasSelection() const {
639 	return _hasSelection.events();
640 }
641 
isEmpty() const642 rpl::producer<bool> Rows::isEmpty() const {
643 	return _isEmpty.events_starting_with(
644 		count() == 0
645 	) | rpl::distinct_until_changed();
646 }
647 
repaint(Selection selected)648 void Rows::repaint(Selection selected) {
649 	v::match(selected, [](v::null_t) {
650 	}, [&](const auto &data) {
651 		repaint(data.index);
652 	});
653 }
654 
repaint(int index)655 void Rows::repaint(int index) {
656 	if (index >= 0) {
657 		repaint(rowByIndex(index));
658 	}
659 }
660 
repaint(const Row & row)661 void Rows::repaint(const Row &row) {
662 	update(0, row.top, width(), row.height);
663 }
664 
repaintChecked(not_null<const Row * > row)665 void Rows::repaintChecked(not_null<const Row*> row) {
666 	const auto found = (ranges::find(_filtered, row) != end(_filtered));
667 	if (_query.isEmpty() || found) {
668 		repaint(*row);
669 	}
670 }
671 
updateSelected(Selection selected)672 void Rows::updateSelected(Selection selected) {
673 	const auto changed = (v::is_null(_selected) != v::is_null(selected));
674 	repaint(_selected);
675 	_selected = selected;
676 	repaint(_selected);
677 	if (changed) {
678 		_hasSelection.fire(!v::is_null(_selected));
679 	}
680 }
681 
updatePressed(Selection pressed)682 void Rows::updatePressed(Selection pressed) {
683 	if (!v::is_null(_pressed)) {
684 		if (!rowBySelection(_pressed).menuToggleForceRippled) {
685 			if (const auto ripple = rippleBySelection(_pressed).get()) {
686 				ripple->lastStop();
687 			}
688 		}
689 	}
690 	_pressed = pressed;
691 }
692 
rowByIndex(int index)693 Rows::Row &Rows::rowByIndex(int index) {
694 	Expects(index >= 0 && index < count());
695 
696 	return _query.isEmpty() ? _rows[index] : *_filtered[index];
697 }
698 
rowByIndex(int index) const699 const Rows::Row &Rows::rowByIndex(int index) const {
700 	Expects(index >= 0 && index < count());
701 
702 	return _query.isEmpty() ? _rows[index] : *_filtered[index];
703 }
704 
rowBySelection(Selection selected)705 Rows::Row &Rows::rowBySelection(Selection selected) {
706 	return rowByIndex(indexFromSelection(selected));
707 }
708 
rowBySelection(Selection selected) const709 const Rows::Row &Rows::rowBySelection(Selection selected) const {
710 	return rowByIndex(indexFromSelection(selected));
711 }
712 
rippleBySelection(Selection selected)713 std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(
714 		Selection selected) {
715 	return rippleBySelection(&rowBySelection(selected), selected);
716 }
717 
rippleBySelection(Selection selected) const718 const std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(
719 		Selection selected) const {
720 	return rippleBySelection(&rowBySelection(selected), selected);
721 }
722 
rippleBySelection(not_null<Row * > row,Selection selected)723 std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(
724 		not_null<Row*> row,
725 		Selection selected) {
726 	return v::is<MenuSelection>(selected)
727 		? row->menuToggleRipple
728 		: row->ripple;
729 }
730 
rippleBySelection(not_null<const Row * > row,Selection selected) const731 const std::unique_ptr<Ui::RippleAnimation> &Rows::rippleBySelection(
732 		not_null<const Row*> row,
733 		Selection selected) const {
734 	return const_cast<Rows*>(this)->rippleBySelection(
735 		const_cast<Row*>(row.get()),
736 		selected);
737 }
738 
rowScrollRequest(int index) const739 Ui::ScrollToRequest Rows::rowScrollRequest(int index) const {
740 	const auto &row = rowByIndex(index);
741 	return Ui::ScrollToRequest(row.top, row.top + row.height);
742 }
743 
DefaultRowHeight()744 int Rows::DefaultRowHeight() {
745 	return st::passportRowPadding.top()
746 		+ st::semiboldFont->height
747 		+ st::passportRowSkip
748 		+ st::normalFont->height
749 		+ st::passportRowPadding.bottom();
750 }
751 
resizeGetHeight(int newWidth)752 int Rows::resizeGetHeight(int newWidth) {
753 	const auto availableWidth = countAvailableWidth(newWidth);
754 	auto result = 0;
755 	for (auto i = 0, till = count(); i != till; ++i) {
756 		auto &row = rowByIndex(i);
757 		row.top = result;
758 		row.titleHeight = row.title.countHeight(availableWidth);
759 		row.descriptionHeight = row.description.countHeight(availableWidth);
760 		row.height = st::passportRowPadding.top()
761 			+ row.titleHeight
762 			+ st::passportRowSkip
763 			+ row.descriptionHeight
764 			+ st::passportRowPadding.bottom();
765 		result += row.height;
766 	}
767 	return result;
768 }
769 
countAvailableWidth(int newWidth) const770 int Rows::countAvailableWidth(int newWidth) const {
771 	const auto right = width() - menuToggleArea().x();
772 	return newWidth
773 		- st::passportRowPadding.left()
774 		- st::langsRadio.diameter
775 		- st::passportRowPadding.left()
776 		- right
777 		- st::passportRowIconSkip;
778 }
779 
countAvailableWidth() const780 int Rows::countAvailableWidth() const {
781 	return countAvailableWidth(width());
782 }
783 
paintEvent(QPaintEvent * e)784 void Rows::paintEvent(QPaintEvent *e) {
785 	Painter p(this);
786 
787 	const auto clip = e->rect();
788 
789 	const auto checkLeft = st::passportRowPadding.left();
790 	const auto left = checkLeft
791 		+ st::langsRadio.diameter
792 		+ st::passportRowPadding.left();
793 	const auto availableWidth = countAvailableWidth();
794 	const auto menu = menuToggleArea();
795 	const auto selectedIndex = (_menuShownIndex >= 0)
796 		? _menuShownIndex
797 		: indexFromSelection(!v::is_null(_pressed) ? _pressed : _selected);
798 	for (auto i = 0, till = count(); i != till; ++i) {
799 		const auto &row = rowByIndex(i);
800 		if (row.top + row.height <= clip.y()) {
801 			continue;
802 		} else if (row.top >= clip.y() + clip.height()) {
803 			break;
804 		}
805 		p.setOpacity(row.removed ? st::stickersRowDisabledOpacity : 1.);
806 		p.translate(0, row.top);
807 		const auto guard = gsl::finally([&] { p.translate(0, -row.top); });
808 
809 		const auto selected = (selectedIndex == i);
810 		if (selected && !row.removed) {
811 			p.fillRect(0, 0, width(), row.height, st::windowBgOver);
812 		}
813 
814 		if (row.ripple) {
815 			row.ripple->paint(p, 0, 0, width());
816 			if (row.ripple->empty()) {
817 				row.ripple.reset();
818 			}
819 		}
820 
821 		const auto checkTop = (row.height - st::defaultRadio.diameter) / 2;
822 		row.check->paint(p, checkLeft, checkTop, width());
823 
824 		auto top = st::passportRowPadding.top();
825 
826 		p.setPen(st::passportRowTitleFg);
827 		row.title.drawLeft(p, left, top, availableWidth, width());
828 		top += row.titleHeight + st::passportRowSkip;
829 
830 		p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);
831 		row.description.drawLeft(p, left, top, availableWidth, width());
832 		top += row.descriptionHeight + st::passportRowPadding.bottom();
833 
834 		if (hasMenu(&row)) {
835 			p.setOpacity(1.);
836 			if (selected && row.removed) {
837 				PainterHighQualityEnabler hq(p);
838 				p.setPen(Qt::NoPen);
839 				p.setBrush(st::windowBgOver);
840 				p.drawEllipse(menu);
841 			}
842 			if (row.menuToggleRipple) {
843 				row.menuToggleRipple->paint(p, menu.x(), menu.y(), width());
844 				if (row.menuToggleRipple->empty()) {
845 					row.menuToggleRipple.reset();
846 				}
847 			}
848 			(selected
849 				? st::topBarMenuToggle.iconOver
850 				: st::topBarMenuToggle.icon).paintInCenter(p, menu);
851 		}
852 	}
853 }
854 
Content(QWidget * parent,const Languages & recent,const Languages & official)855 Content::Content(
856 	QWidget *parent,
857 	const Languages &recent,
858 	const Languages &official)
859 : RpWidget(parent) {
860 	setupContent(recent, official);
861 }
862 
setupContent(const Languages & recent,const Languages & official)863 void Content::setupContent(
864 		const Languages &recent,
865 		const Languages &official) {
866 	using namespace rpl::mappers;
867 
868 	const auto current = Lang::LanguageIdOrDefault(Lang::Id());
869 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
870 	const auto add = [&](const Languages &list, bool areOfficial) {
871 		if (list.empty()) {
872 			return (Rows*)nullptr;
873 		}
874 		const auto wrap = content->add(
875 			object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
876 				content,
877 				object_ptr<Ui::VerticalLayout>(content)));
878 		const auto inner = wrap->entity();
879 		inner->add(object_ptr<Ui::FixedHeightWidget>(
880 			inner,
881 			st::defaultBox.margin.top()));
882 		const auto rows = inner->add(object_ptr<Rows>(
883 			inner,
884 			list,
885 			current,
886 			areOfficial));
887 		inner->add(object_ptr<Ui::FixedHeightWidget>(
888 			inner,
889 			st::defaultBox.margin.top()));
890 
891 		rows->isEmpty() | rpl::start_with_next([=](bool empty) {
892 			wrap->toggle(!empty, anim::type::instant);
893 		}, rows->lifetime());
894 
895 		return rows;
896 	};
897 	const auto main = add(recent, false);
898 	const auto divider = content->add(
899 		object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
900 			content,
901 			object_ptr<Ui::BoxContentDivider>(content)));
902 	const auto other = add(official, true);
903 	Ui::ResizeFitChild(this, content);
904 
905 	if (main && other) {
906 		rpl::combine(
907 			main->isEmpty(),
908 			other->isEmpty(),
909 			_1 || _2
910 		) | rpl::start_with_next([=](bool empty) {
911 			divider->toggle(!empty, anim::type::instant);
912 		}, divider->lifetime());
913 
914 		const auto excludeSelections = [](Rows *a, Rows *b) {
915 			a->hasSelection(
916 			) | rpl::filter(
917 				_1
918 			) | rpl::start_with_next([=] {
919 				b->setSelected(-1);
920 			}, a->lifetime());
921 		};
922 		excludeSelections(main, other);
923 		excludeSelections(other, main);
924 	} else {
925 		divider->hide(anim::type::instant);
926 	}
927 
928 	const auto count = [](Rows *widget) {
929 		return widget ? widget->count() : 0;
930 	};
931 	const auto selected = [](Rows *widget) {
932 		return widget ? widget->selected() : -1;
933 	};
934 	const auto rowsCount = [=] {
935 		return count(main) + count(other);
936 	};
937 	const auto selectedIndex = [=] {
938 		if (const auto index = selected(main); index >= 0) {
939 			return index;
940 		} else if (const auto index = selected(other); index >= 0) {
941 			return count(main) + index;
942 		}
943 		return -1;
944 	};
945 	const auto setSelectedIndex = [=](int index) {
946 		const auto first = count(main);
947 		if (index >= first) {
948 			if (main) {
949 				main->setSelected(-1);
950 			}
951 			if (other) {
952 				other->setSelected(index - first);
953 			}
954 		} else {
955 			if (main) {
956 				main->setSelected(index);
957 			}
958 			if (other) {
959 				other->setSelected(-1);
960 			}
961 		}
962 	};
963 	const auto selectedCoords = [=] {
964 		const auto coords = [=](Rows *rows, int index) {
965 			const auto result = rows->rowScrollRequest(index);
966 			const auto shift = rows->mapToGlobal({ 0, 0 }).y()
967 				- mapToGlobal({ 0, 0 }).y();
968 			return Ui::ScrollToRequest(
969 				result.ymin + shift,
970 				result.ymax + shift);
971 		};
972 		if (const auto index = selected(main); index >= 0) {
973 			return coords(main, index);
974 		} else if (const auto index = selected(other); index >= 0) {
975 			return coords(other, index);
976 		}
977 		return Ui::ScrollToRequest(-1, -1);
978 	};
979 	_jump = [=](int rows) {
980 		const auto count = rowsCount();
981 		const auto now = selectedIndex();
982 		if (now >= 0) {
983 			const auto changed = now + rows;
984 			if (changed < 0) {
985 				setSelectedIndex((now > 0) ? 0 : -1);
986 			} else if (changed >= count) {
987 				setSelectedIndex(count - 1);
988 			} else {
989 				setSelectedIndex(changed);
990 			}
991 		} else if (rows > 0) {
992 			setSelectedIndex(0);
993 		}
994 		return selectedCoords();
995 	};
996 	const auto filter = [](Rows *widget, const QString &query) {
997 		if (widget) {
998 			widget->filter(query);
999 		}
1000 	};
1001 	_filter = [=](const QString &query) {
1002 		filter(main, query);
1003 		filter(other, query);
1004 	};
1005 	_activations = [=] {
1006 		if (!main && !other) {
1007 			return rpl::never<Language>() | rpl::type_erased();
1008 		} else if (!main) {
1009 			return other->activations();
1010 		} else if (!other) {
1011 			return main->activations();
1012 		}
1013 		return rpl::merge(
1014 			main->activations(),
1015 			other->activations()
1016 		) | rpl::type_erased();
1017 	};
1018 	_changeChosen = [=](const QString &chosen) {
1019 		if (main) {
1020 			main->changeChosen(chosen);
1021 		}
1022 		if (other) {
1023 			other->changeChosen(chosen);
1024 		}
1025 	};
1026 	_activateBySubmit = [=] {
1027 		if (selectedIndex() < 0) {
1028 			_jump(1);
1029 		}
1030 		if (main) {
1031 			main->activateSelected();
1032 		}
1033 		if (other) {
1034 			other->activateSelected();
1035 		}
1036 	};
1037 }
1038 
filter(const QString & query)1039 void Content::filter(const QString &query) {
1040 	_filter(query);
1041 }
1042 
activations() const1043 rpl::producer<Language> Content::activations() const {
1044 	return _activations();
1045 }
1046 
changeChosen(const QString & chosen)1047 void Content::changeChosen(const QString &chosen) {
1048 	_changeChosen(chosen);
1049 }
1050 
activateBySubmit()1051 void Content::activateBySubmit() {
1052 	_activateBySubmit();
1053 }
1054 
jump(int rows)1055 Ui::ScrollToRequest Content::jump(int rows) {
1056 	return _jump(rows);
1057 }
1058 
1059 } // namespace
1060 
prepare()1061 void LanguageBox::prepare() {
1062 	addButton(tr::lng_box_ok(), [=] { closeBox(); });
1063 
1064 	setTitle(tr::lng_languages());
1065 
1066 	const auto select = createMultiSelect();
1067 
1068 	using namespace rpl::mappers;
1069 
1070 	const auto [recent, official] = PrepareLists();
1071 	const auto inner = setInnerWidget(
1072 		object_ptr<Content>(this, recent, official),
1073 		st::boxScroll,
1074 		select->height());
1075 	inner->resizeToWidth(st::boxWidth);
1076 
1077 	const auto max = lifetime().make_state<int>(0);
1078 	rpl::combine(
1079 		inner->heightValue(),
1080 		select->heightValue(),
1081 		_1 + _2
1082 	) | rpl::start_with_next([=](int height) {
1083 		accumulate_max(*max, height);
1084 		setDimensions(st::boxWidth, qMin(*max, st::boxMaxListHeight));
1085 	}, inner->lifetime());
1086 
1087 	select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
1088 		inner->activateBySubmit();
1089 	});
1090 	select->setQueryChangedCallback([=](const QString &query) {
1091 		inner->filter(query);
1092 	});
1093 	select->setCancelledCallback([=] {
1094 		select->clearQuery();
1095 	});
1096 
1097 	inner->activations(
1098 	) | rpl::start_with_next([=](const Language &language) {
1099 		// "#custom" is applied each time it's passed to switchToLanguage().
1100 		// So we check that the language really has changed.
1101 		const auto currentId = [] {
1102 			return Lang::LanguageIdOrDefault(Lang::Id());
1103 		};
1104 		if (language.id != currentId()) {
1105 			Lang::CurrentCloudManager().switchToLanguage(language);
1106 			if (inner) {
1107 				inner->changeChosen(currentId());
1108 			}
1109 		}
1110 	}, inner->lifetime());
1111 
1112 	_setInnerFocus = [=] {
1113 		select->setInnerFocus();
1114 	};
1115 	_jump = [=](int rows) {
1116 		return inner->jump(rows);
1117 	};
1118 }
1119 
keyPressEvent(QKeyEvent * e)1120 void LanguageBox::keyPressEvent(QKeyEvent *e) {
1121 	const auto key = e->key();
1122 	if (key == Qt::Key_Escape) {
1123 		closeBox();
1124 		return;
1125 	}
1126 	const auto selected = [&] {
1127 		if (key == Qt::Key_Up) {
1128 			return _jump(-1);
1129 		} else if (key == Qt::Key_Down) {
1130 			return _jump(1);
1131 		} else if (key == Qt::Key_PageUp) {
1132 			return _jump(-rowsInPage());
1133 		} else if (key == Qt::Key_PageDown) {
1134 			return _jump(rowsInPage());
1135 		}
1136 		return Ui::ScrollToRequest(-1, -1);
1137 	}();
1138 	if (selected.ymin >= 0 && selected.ymax >= 0) {
1139 		onScrollToY(selected.ymin, selected.ymax);
1140 	}
1141 }
1142 
rowsInPage() const1143 int LanguageBox::rowsInPage() const {
1144 	return std::max(height() / Rows::DefaultRowHeight(), 1);
1145 }
1146 
setInnerFocus()1147 void LanguageBox::setInnerFocus() {
1148 	_setInnerFocus();
1149 }
1150 
createMultiSelect()1151 not_null<Ui::MultiSelect*> LanguageBox::createMultiSelect() {
1152 	const auto result = Ui::CreateChild<Ui::MultiSelect>(
1153 		this,
1154 		st::defaultMultiSelect,
1155 		tr::lng_participant_filter());
1156 	result->resizeToWidth(st::boxWidth);
1157 	result->moveToLeft(0, 0);
1158 	return result;
1159 }
1160 
Show()1161 base::binary_guard LanguageBox::Show() {
1162 	auto result = base::binary_guard();
1163 
1164 	auto &manager = Lang::CurrentCloudManager();
1165 	if (manager.languageList().empty()) {
1166 		auto guard = std::make_shared<base::binary_guard>(
1167 			result.make_guard());
1168 		auto lifetime = std::make_shared<rpl::lifetime>();
1169 		manager.languageListChanged(
1170 		) | rpl::take(
1171 			1
1172 		) | rpl::start_with_next([=]() mutable {
1173 			const auto show = guard->alive();
1174 			if (lifetime) {
1175 				base::take(lifetime)->destroy();
1176 			}
1177 			if (show) {
1178 				Ui::show(Box<LanguageBox>());
1179 			}
1180 		}, *lifetime);
1181 	} else {
1182 		Ui::show(Box<LanguageBox>());
1183 	}
1184 	manager.requestLanguageList();
1185 
1186 	return result;
1187 }
1188