intro_code_input.cpp 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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 "intro/intro_code_input.h"
  8. #include "lang/lang_keys.h"
  9. #include "ui/abstract_button.h"
  10. #include "ui/effects/shake_animation.h"
  11. #include "ui/painter.h"
  12. #include "ui/rect.h"
  13. #include "ui/widgets/popup_menu.h"
  14. #include "styles/style_basic.h"
  15. #include "styles/style_intro.h"
  16. #include "styles/style_layers.h" // boxRadius
  17. #include <QtCore/QRegularExpression>
  18. #include <QtGui/QClipboard>
  19. #include <QtGui/QGuiApplication>
  20. namespace Ui {
  21. namespace {
  22. constexpr auto kDigitNone = int(-1);
  23. [[nodiscard]] int Circular(int left, int right) {
  24. return ((left % right) + right) % right;
  25. }
  26. class Shaker final {
  27. public:
  28. explicit Shaker(not_null<Ui::RpWidget*> widget);
  29. void shake();
  30. private:
  31. const not_null<Ui::RpWidget*> _widget;
  32. Ui::Animations::Simple _animation;
  33. };
  34. Shaker::Shaker(not_null<Ui::RpWidget*> widget)
  35. : _widget(widget) {
  36. }
  37. void Shaker::shake() {
  38. if (_animation.animating()) {
  39. return;
  40. }
  41. _animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) {
  42. _widget->moveToLeft(x + shift, _widget->y());
  43. }), 0., 1., st::shakeDuration);
  44. }
  45. } // namespace
  46. class CodeDigit final : public Ui::AbstractButton {
  47. public:
  48. explicit CodeDigit(not_null<Ui::RpWidget*> widget);
  49. void setDigit(int digit);
  50. [[nodiscard]] int digit() const;
  51. void setBorderColor(const QBrush &brush);
  52. void shake();
  53. protected:
  54. void paintEvent(QPaintEvent *e) override;
  55. private:
  56. Shaker _shaker;
  57. Ui::Animations::Simple _animation;
  58. int _dataDigit = kDigitNone;
  59. int _viewDigit = kDigitNone;
  60. QPen _borderPen;
  61. };
  62. CodeDigit::CodeDigit(not_null<Ui::RpWidget*> widget)
  63. : Ui::AbstractButton(widget)
  64. , _shaker(this) {
  65. setBorderColor(st::windowBgRipple);
  66. }
  67. void CodeDigit::setDigit(int digit) {
  68. if ((_dataDigit == digit) && _animation.animating()) {
  69. return;
  70. }
  71. _dataDigit = digit;
  72. if (_viewDigit != digit) {
  73. _animation.stop();
  74. if (digit == kDigitNone) {
  75. _animation.start([=](float64 value) {
  76. update();
  77. if (!value) {
  78. _viewDigit = digit;
  79. }
  80. }, 1., 0., st::universalDuration);
  81. } else {
  82. _viewDigit = digit;
  83. _animation.start([=] { update(); }, 0, 1., st::universalDuration);
  84. }
  85. }
  86. }
  87. int CodeDigit::digit() const {
  88. return _dataDigit;
  89. }
  90. void CodeDigit::setBorderColor(const QBrush &brush) {
  91. _borderPen = QPen(brush, st::introCodeDigitBorderWidth);
  92. update();
  93. }
  94. void CodeDigit::shake() {
  95. _shaker.shake();
  96. }
  97. void CodeDigit::paintEvent(QPaintEvent *e) {
  98. auto p = QPainter(this);
  99. auto clipPath = QPainterPath();
  100. clipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius);
  101. p.setClipPath(clipPath);
  102. p.fillRect(rect(), st::windowBgOver);
  103. {
  104. auto hq = PainterHighQualityEnabler(p);
  105. p.strokePath(clipPath, _borderPen);
  106. }
  107. if (_viewDigit == kDigitNone) {
  108. return;
  109. }
  110. const auto hiding = (_dataDigit == kDigitNone);
  111. const auto progress = _animation.value(1.);
  112. if (hiding) {
  113. p.setOpacity(progress * progress);
  114. const auto center = rect().center();
  115. p.setTransform(QTransform()
  116. .translate(center.x(), center.y())
  117. .scale(progress, progress)
  118. .translate(-center.x(), -center.y()));
  119. } else {
  120. p.setOpacity(progress);
  121. constexpr auto kSlideDistanceRatio = 0.2;
  122. const auto distance = rect().height() * kSlideDistanceRatio;
  123. p.translate(0, (distance * (1. - progress)));
  124. }
  125. p.setFont(st::introCodeDigitFont);
  126. p.setPen(st::windowFg);
  127. p.drawText(rect(), QString::number(_viewDigit), style::al_center);
  128. }
  129. CodeInput::CodeInput(QWidget *parent)
  130. : Ui::RpWidget(parent) {
  131. setFocusPolicy(Qt::StrongFocus);
  132. }
  133. void CodeInput::setDigitsCountMax(int digitsCount) {
  134. _digitsCountMax = digitsCount;
  135. _digits.clear();
  136. _currentIndex = 0;
  137. constexpr auto kWidthRatio = 0.8;
  138. const auto digitWidth = st::introCodeDigitHeight * kWidthRatio;
  139. const auto padding = Margins(st::introCodeDigitSkip);
  140. resize(
  141. padding.left()
  142. + digitWidth * digitsCount
  143. + st::introCodeDigitSkip * (digitsCount - 1)
  144. + padding.right(),
  145. st::introCodeDigitHeight);
  146. for (auto i = 0; i < digitsCount; i++) {
  147. const auto widget = Ui::CreateChild<CodeDigit>(this);
  148. widget->setPointerCursor(false);
  149. widget->setClickedCallback([=] { unfocusAll(_currentIndex = i); });
  150. widget->resize(digitWidth, st::introCodeDigitHeight);
  151. widget->moveToLeft(
  152. padding.left() + (digitWidth + st::introCodeDigitSkip) * i,
  153. 0);
  154. _digits.emplace_back(widget);
  155. }
  156. }
  157. void CodeInput::setCode(QString code) {
  158. using namespace TextUtilities;
  159. code = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax);
  160. for (int i = 0; i < _digits.size(); i++) {
  161. if (i >= code.size()) {
  162. return;
  163. }
  164. _digits[i]->setDigit(code.at(i).digitValue());
  165. }
  166. }
  167. void CodeInput::requestCode() {
  168. const auto result = collectDigits();
  169. if (result.size() == _digitsCountMax) {
  170. _codeCollected.fire_copy(result);
  171. } else {
  172. findEmptyAndPerform([&](int i) { _digits[i]->shake(); });
  173. }
  174. }
  175. rpl::producer<QString> CodeInput::codeCollected() const {
  176. return _codeCollected.events();
  177. }
  178. void CodeInput::clear() {
  179. for (const auto &digit : _digits) {
  180. digit->setDigit(kDigitNone);
  181. }
  182. unfocusAll(_currentIndex = 0);
  183. }
  184. void CodeInput::showError() {
  185. clear();
  186. for (const auto &digit : _digits) {
  187. digit->shake();
  188. digit->setBorderColor(st::activeLineFgError);
  189. }
  190. }
  191. void CodeInput::focusInEvent(QFocusEvent *e) {
  192. unfocusAll(_currentIndex);
  193. }
  194. void CodeInput::focusOutEvent(QFocusEvent *e) {
  195. unfocusAll(kDigitNone);
  196. }
  197. void CodeInput::paintEvent(QPaintEvent *e) {
  198. auto p = QPainter(this);
  199. p.fillRect(rect(), st::windowBg);
  200. }
  201. void CodeInput::keyPressEvent(QKeyEvent *e) {
  202. const auto key = e->key();
  203. if (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) {
  204. _currentIndex = Circular(_currentIndex + 1, _digits.size());
  205. unfocusAll(_currentIndex);
  206. } else if (key == Qt::Key_Up || key == Qt::Key_Left) {
  207. _currentIndex = Circular(_currentIndex - 1, _digits.size());
  208. unfocusAll(_currentIndex);
  209. } else if (key >= Qt::Key_0 && key <= Qt::Key_9) {
  210. const auto index = int(key - Qt::Key_0);
  211. _digits[_currentIndex]->setDigit(index);
  212. _currentIndex = Circular(_currentIndex + 1, _digits.size());
  213. if (!_currentIndex) {
  214. const auto result = collectDigits();
  215. if (result.size() == _digitsCountMax) {
  216. _codeCollected.fire_copy(result);
  217. _currentIndex = _digits.size() - 1;
  218. } else {
  219. findEmptyAndPerform([&](int i) { _currentIndex = i; });
  220. }
  221. }
  222. unfocusAll(_currentIndex);
  223. } else if (key == Qt::Key_Delete) {
  224. _digits[_currentIndex]->setDigit(kDigitNone);
  225. } else if (key == Qt::Key_Backspace) {
  226. const auto wasDigit = _digits[_currentIndex]->digit();
  227. _digits[_currentIndex]->setDigit(kDigitNone);
  228. _currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size()));
  229. if (wasDigit == kDigitNone) {
  230. _digits[_currentIndex]->setDigit(kDigitNone);
  231. }
  232. unfocusAll(_currentIndex);
  233. } else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  234. requestCode();
  235. } else if (e == QKeySequence::Paste) {
  236. insertCodeAndSubmit(QGuiApplication::clipboard()->text());
  237. } else if (key >= Qt::Key_A && key <= Qt::Key_Z) {
  238. _digits[_currentIndex]->shake();
  239. } else if (key == Qt::Key_Home || key == Qt::Key_PageUp) {
  240. unfocusAll(_currentIndex = 0);
  241. } else if (key == Qt::Key_End || key == Qt::Key_PageDown) {
  242. unfocusAll(_currentIndex = (_digits.size() - 1));
  243. }
  244. }
  245. void CodeInput::contextMenuEvent(QContextMenuEvent *e) {
  246. if (_menu) {
  247. return;
  248. }
  249. _menu = base::make_unique_q<Ui::PopupMenu>(this, st::defaultPopupMenu);
  250. _menu->addAction(tr::lng_mac_menu_paste(tr::now), [=] {
  251. insertCodeAndSubmit(QGuiApplication::clipboard()->text());
  252. })->setEnabled(!QGuiApplication::clipboard()->text().isEmpty());
  253. _menu->popup(QCursor::pos());
  254. }
  255. void CodeInput::insertCodeAndSubmit(const QString &code) {
  256. if (code.isEmpty()) {
  257. return;
  258. }
  259. setCode(code);
  260. _currentIndex = _digits.size() - 1;
  261. findEmptyAndPerform([&](int i) { _currentIndex = i; });
  262. unfocusAll(_currentIndex);
  263. if ((_currentIndex == _digits.size() - 1)
  264. && _digits[_currentIndex]->digit() != kDigitNone) {
  265. requestCode();
  266. }
  267. }
  268. QString CodeInput::collectDigits() const {
  269. auto result = QString();
  270. for (const auto &digit : _digits) {
  271. if (digit->digit() != kDigitNone) {
  272. result += QString::number(digit->digit());
  273. }
  274. }
  275. return result;
  276. }
  277. void CodeInput::unfocusAll(int except) {
  278. for (auto i = 0; i < _digits.size(); i++) {
  279. const auto focused = (i == except);
  280. _digits[i]->setBorderColor(focused
  281. ? st::windowActiveTextFg
  282. : st::windowBgRipple);
  283. }
  284. }
  285. void CodeInput::findEmptyAndPerform(const Fn<void(int)> &callback) {
  286. for (auto i = 0; i < _digits.size(); i++) {
  287. if (_digits[i]->digit() == kDigitNone) {
  288. callback(i);
  289. break;
  290. }
  291. }
  292. }
  293. } // namespace Ui