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