country_select_box.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/boxes/country_select_box.h"
  8. #include "lang/lang_keys.h"
  9. #include "ui/widgets/scroll_area.h"
  10. #include "ui/widgets/multi_select.h"
  11. #include "ui/effects/ripple_animation.h"
  12. #include "ui/painter.h"
  13. #include "countries/countries_instance.h"
  14. #include "styles/style_layers.h"
  15. #include "styles/style_boxes.h"
  16. #include "styles/style_intro.h"
  17. #include <QtCore/QRegularExpression>
  18. namespace Ui {
  19. namespace {
  20. QString LastValidISO;
  21. } // namespace
  22. class CountrySelectBox::Inner : public RpWidget {
  23. public:
  24. Inner(QWidget *parent, const QString &iso, Type type);
  25. ~Inner();
  26. void updateFilter(QString filter = QString());
  27. void selectSkip(int32 dir);
  28. void selectSkipPage(int32 h, int32 dir);
  29. void chooseCountry();
  30. void refresh();
  31. [[nodiscard]] rpl::producer<Entry> countryChosen() const {
  32. return _countryChosen.events();
  33. }
  34. [[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {
  35. return _mustScrollTo.events();
  36. }
  37. protected:
  38. void paintEvent(QPaintEvent *e) override;
  39. void enterEventHook(QEnterEvent *e) override;
  40. void leaveEventHook(QEvent *e) override;
  41. void mouseMoveEvent(QMouseEvent *e) override;
  42. void mousePressEvent(QMouseEvent *e) override;
  43. void mouseReleaseEvent(QMouseEvent *e) override;
  44. private:
  45. void init();
  46. void updateSelected() {
  47. updateSelected(mapFromGlobal(QCursor::pos()));
  48. }
  49. void updateSelected(QPoint localPos);
  50. void updateSelectedRow();
  51. void updateRow(int index);
  52. void setPressed(int pressed);
  53. const std::vector<Entry> &current() const;
  54. Type _type = Type::Phones;
  55. int _rowHeight = 0;
  56. int _selected = -1;
  57. int _pressed = -1;
  58. QString _filter;
  59. bool _mouseSelection = false;
  60. std::vector<std::unique_ptr<RippleAnimation>> _ripples;
  61. std::vector<Entry> _list;
  62. std::vector<Entry> _filtered;
  63. base::flat_map<QChar, std::vector<int>> _byLetter;
  64. std::vector<std::vector<QString>> _namesList;
  65. rpl::event_stream<Entry> _countryChosen;
  66. rpl::event_stream<ScrollToRequest> _mustScrollTo;
  67. };
  68. CountrySelectBox::CountrySelectBox(QWidget*)
  69. : CountrySelectBox(nullptr, QString(), Type::Phones) {
  70. }
  71. CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
  72. : _select(this, st::defaultMultiSelect, tr::lng_country_ph())
  73. , _ownedInner(this, iso, type) {
  74. }
  75. rpl::producer<QString> CountrySelectBox::countryChosen() const {
  76. Expects(_ownedInner != nullptr || _inner != nullptr);
  77. return (_ownedInner
  78. ? _ownedInner.data()
  79. : _inner.data())->countryChosen() | rpl::map([](const Entry &e) {
  80. return e.iso2;
  81. });
  82. }
  83. rpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {
  84. Expects(_ownedInner != nullptr || _inner != nullptr);
  85. return (_ownedInner
  86. ? _ownedInner.data()
  87. : _inner.data())->countryChosen();
  88. }
  89. void CountrySelectBox::prepare() {
  90. setTitle(tr::lng_country_select());
  91. _select->resizeToWidth(st::boxWidth);
  92. _select->setQueryChangedCallback([=](const QString &query) {
  93. applyFilterUpdate(query);
  94. });
  95. _select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
  96. submit();
  97. });
  98. _inner = setInnerWidget(
  99. std::move(_ownedInner),
  100. st::countriesScroll,
  101. _select->height());
  102. addButton(tr::lng_close(), [=] { closeBox(); });
  103. setDimensions(st::boxWidth, st::boxMaxListHeight);
  104. _inner->mustScrollTo(
  105. ) | rpl::start_with_next([=](ScrollToRequest request) {
  106. scrollToY(request.ymin, request.ymax);
  107. }, lifetime());
  108. }
  109. void CountrySelectBox::submit() {
  110. _inner->chooseCountry();
  111. }
  112. void CountrySelectBox::keyPressEvent(QKeyEvent *e) {
  113. if (e->key() == Qt::Key_Down) {
  114. _inner->selectSkip(1);
  115. } else if (e->key() == Qt::Key_Up) {
  116. _inner->selectSkip(-1);
  117. } else if (e->key() == Qt::Key_PageDown) {
  118. _inner->selectSkipPage(height() - _select->height(), 1);
  119. } else if (e->key() == Qt::Key_PageUp) {
  120. _inner->selectSkipPage(height() - _select->height(), -1);
  121. } else {
  122. BoxContent::keyPressEvent(e);
  123. }
  124. }
  125. void CountrySelectBox::resizeEvent(QResizeEvent *e) {
  126. BoxContent::resizeEvent(e);
  127. _select->resizeToWidth(width());
  128. _select->moveToLeft(0, 0);
  129. _inner->resizeToWidth(width());
  130. }
  131. void CountrySelectBox::applyFilterUpdate(const QString &query) {
  132. scrollToY(0);
  133. _inner->updateFilter(query);
  134. }
  135. void CountrySelectBox::setInnerFocus() {
  136. _select->setInnerFocus();
  137. }
  138. CountrySelectBox::Inner::Inner(
  139. QWidget *parent,
  140. const QString &iso,
  141. Type type)
  142. : RpWidget(parent)
  143. , _type(type)
  144. , _rowHeight(st::countryRowHeight) {
  145. setAttribute(Qt::WA_OpaquePaintEvent);
  146. const auto &byISO2 = Countries::Instance().byISO2();
  147. if (byISO2.contains(iso)) {
  148. LastValidISO = iso;
  149. }
  150. rpl::single(
  151. ) | rpl::then(
  152. Countries::Instance().updated()
  153. ) | rpl::start_with_next([=] {
  154. _mustScrollTo.fire(ScrollToRequest(0, 0));
  155. _list.clear();
  156. _namesList.clear();
  157. init();
  158. const auto filter = _filter;
  159. _filter = u"a"_q;
  160. updateFilter(filter);
  161. }, lifetime());
  162. }
  163. void CountrySelectBox::Inner::init() {
  164. const auto &byISO2 = Countries::Instance().byISO2();
  165. const auto extractEntries = [&](const Countries::Info &info) {
  166. for (const auto &code : info.codes) {
  167. _list.push_back(Entry{
  168. .country = info.name,
  169. .iso2 = info.iso2,
  170. .code = code.callingCode,
  171. .alternativeName = info.alternativeName,
  172. });
  173. }
  174. };
  175. _list.reserve(byISO2.size());
  176. _namesList.reserve(byISO2.size());
  177. const auto l = byISO2.constFind(LastValidISO);
  178. const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;
  179. if (lastValid) {
  180. extractEntries(*lastValid);
  181. }
  182. for (const auto &entry : Countries::Instance().list()) {
  183. if (&entry != lastValid) {
  184. extractEntries(entry);
  185. }
  186. }
  187. auto index = 0;
  188. for (const auto &info : _list) {
  189. static const auto RegExp = QRegularExpression("[\\s\\-]");
  190. auto full = info.country
  191. + ' '
  192. + (!info.alternativeName.isEmpty()
  193. ? info.alternativeName
  194. : QString());
  195. const auto namesList = std::move(full).toLower().split(
  196. RegExp,
  197. Qt::SkipEmptyParts);
  198. auto &names = _namesList.emplace_back();
  199. names.reserve(namesList.size());
  200. for (const auto &name : namesList) {
  201. const auto part = name.trimmed();
  202. if (part.isEmpty()) {
  203. continue;
  204. }
  205. const auto ch = part[0];
  206. auto &byLetter = _byLetter[ch];
  207. if (byLetter.empty() || byLetter.back() != index) {
  208. byLetter.push_back(index);
  209. }
  210. names.push_back(part);
  211. }
  212. ++index;
  213. }
  214. }
  215. void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
  216. Painter p(this);
  217. QRect r(e->rect());
  218. p.setClipRect(r);
  219. const auto &list = current();
  220. if (list.empty()) {
  221. p.fillRect(r, st::boxBg);
  222. p.setFont(st::noContactsFont);
  223. p.setPen(st::noContactsColor);
  224. p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);
  225. return;
  226. }
  227. const auto l = int(list.size());
  228. if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {
  229. p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);
  230. }
  231. int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);
  232. int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);
  233. for (int32 i = from; i < to; ++i) {
  234. auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
  235. auto y = st::countriesSkip + i * _rowHeight;
  236. p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);
  237. if (_ripples.size() > i && _ripples[i]) {
  238. _ripples[i]->paint(p, 0, y, width());
  239. if (_ripples[i]->empty()) {
  240. _ripples[i].reset();
  241. }
  242. }
  243. auto code = QString("+") + list[i].code;
  244. auto codeWidth = st::countryRowCodeFont->width(code);
  245. auto name = list[i].country;
  246. auto nameWidth = st::countryRowNameFont->width(name);
  247. auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;
  248. if (nameWidth > availWidth) {
  249. name = st::countryRowNameFont->elided(name, availWidth);
  250. nameWidth = st::countryRowNameFont->width(name);
  251. }
  252. p.setFont(st::countryRowNameFont);
  253. p.setPen(st::countryRowNameFg);
  254. p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);
  255. if (_type == Type::Phones) {
  256. p.setFont(st::countryRowCodeFont);
  257. p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);
  258. p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);
  259. }
  260. }
  261. }
  262. void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
  263. setMouseTracking(true);
  264. }
  265. void CountrySelectBox::Inner::leaveEventHook(QEvent *e) {
  266. _mouseSelection = false;
  267. setMouseTracking(false);
  268. if (_selected >= 0) {
  269. updateSelectedRow();
  270. _selected = -1;
  271. }
  272. }
  273. void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {
  274. _mouseSelection = true;
  275. updateSelected(e->pos());
  276. }
  277. void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {
  278. _mouseSelection = true;
  279. updateSelected(e->pos());
  280. setPressed(_selected);
  281. const auto &list = current();
  282. if (_pressed >= 0 && _pressed < list.size()) {
  283. if (_ripples.size() <= _pressed) {
  284. _ripples.reserve(_pressed + 1);
  285. while (_ripples.size() <= _pressed) {
  286. _ripples.push_back(nullptr);
  287. }
  288. }
  289. if (!_ripples[_pressed]) {
  290. auto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));
  291. _ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {
  292. updateRow(index);
  293. });
  294. _ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));
  295. }
  296. }
  297. }
  298. void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
  299. auto pressed = _pressed;
  300. setPressed(-1);
  301. updateSelectedRow();
  302. if (e->button() == Qt::LeftButton) {
  303. if ((pressed >= 0) && pressed == _selected) {
  304. chooseCountry();
  305. }
  306. }
  307. }
  308. void CountrySelectBox::Inner::updateFilter(QString filter) {
  309. const auto words = TextUtilities::PrepareSearchWords(filter);
  310. filter = words.isEmpty() ? QString() : words.join(' ');
  311. if (_filter == filter) {
  312. return;
  313. }
  314. _filter = filter;
  315. const auto findWord = [&](
  316. const std::vector<QString> &names,
  317. const QString &word) {
  318. for (const auto &name : names) {
  319. if (name.startsWith(word)) {
  320. return true;
  321. }
  322. }
  323. return false;
  324. };
  325. const auto hasAllWords = [&](const std::vector<QString> &names) {
  326. for (const auto &word : words) {
  327. if (!findWord(names, word)) {
  328. return false;
  329. }
  330. }
  331. return true;
  332. };
  333. if (!_filter.isEmpty()) {
  334. _filtered.clear();
  335. for (const auto index : _byLetter[_filter[0].toLower()]) {
  336. if (hasAllWords(_namesList[index])) {
  337. _filtered.push_back(_list[index]);
  338. }
  339. }
  340. }
  341. refresh();
  342. _selected = current().empty() ? -1 : 0;
  343. update();
  344. }
  345. void CountrySelectBox::Inner::selectSkip(int32 dir) {
  346. _mouseSelection = false;
  347. const auto &list = current();
  348. int cur = (_selected >= 0) ? _selected : -1;
  349. cur += dir;
  350. if (cur <= 0) {
  351. _selected = list.empty() ? -1 : 0;
  352. } else if (cur >= list.size()) {
  353. _selected = -1;
  354. } else {
  355. _selected = cur;
  356. }
  357. if (_selected >= 0) {
  358. _mustScrollTo.fire(ScrollToRequest(
  359. st::countriesSkip + _selected * _rowHeight,
  360. st::countriesSkip + (_selected + 1) * _rowHeight));
  361. }
  362. update();
  363. }
  364. void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
  365. int32 points = h / _rowHeight;
  366. if (!points) return;
  367. selectSkip(points * dir);
  368. }
  369. void CountrySelectBox::Inner::chooseCountry() {
  370. const auto &list = current();
  371. _countryChosen.fire_copy((_selected >= 0 && _selected < list.size())
  372. ? list[_selected]
  373. : Entry());
  374. }
  375. void CountrySelectBox::Inner::refresh() {
  376. const auto &list = current();
  377. resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));
  378. }
  379. void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
  380. if (!_mouseSelection) return;
  381. auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));
  382. const auto &list = current();
  383. auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;
  384. if (_selected != selected) {
  385. updateSelectedRow();
  386. _selected = selected;
  387. updateSelectedRow();
  388. }
  389. }
  390. auto CountrySelectBox::Inner::current() const
  391. -> const std::vector<CountrySelectBox::Entry> & {
  392. return _filter.isEmpty() ? _list : _filtered;
  393. }
  394. void CountrySelectBox::Inner::updateSelectedRow() {
  395. updateRow(_selected);
  396. }
  397. void CountrySelectBox::Inner::updateRow(int index) {
  398. if (index >= 0) {
  399. update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
  400. }
  401. }
  402. void CountrySelectBox::Inner::setPressed(int pressed) {
  403. if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {
  404. _ripples[_pressed]->lastStop();
  405. }
  406. _pressed = pressed;
  407. }
  408. CountrySelectBox::Inner::~Inner() = default;
  409. } // namespace Ui