tdesktop/Telegram/SourceFiles/intro/intro_code_input.cpp

344 lines
8.6 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "intro/intro_code_input.h"
#include "lang/lang_keys.h"
#include "ui/abstract_button.h"
#include "ui/effects/shake_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_basic.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h" // boxRadius
#include <QtCore/QRegularExpression>
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
constexpr auto kDigitNone = int(-1);
[[nodiscard]] int Circular(int left, int right) {
return ((left % right) + right) % right;
}
class Shaker final {
public:
explicit Shaker(not_null<Ui::RpWidget*> widget);
void shake();
private:
const not_null<Ui::RpWidget*> _widget;
Ui::Animations::Simple _animation;
};
Shaker::Shaker(not_null<Ui::RpWidget*> widget)
: _widget(widget) {
}
void Shaker::shake() {
if (_animation.animating()) {
return;
}
_animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) {
_widget->moveToLeft(x + shift, _widget->y());
}), 0., 1., st::shakeDuration);
}
} // namespace
class CodeDigit final : public Ui::AbstractButton {
public:
explicit CodeDigit(not_null<Ui::RpWidget*> widget);
void setDigit(int digit);
[[nodiscard]] int digit() const;
void setBorderColor(const QBrush &brush);
void shake();
protected:
void paintEvent(QPaintEvent *e) override;
private:
Shaker _shaker;
Ui::Animations::Simple _animation;
int _dataDigit = kDigitNone;
int _viewDigit = kDigitNone;
QPen _borderPen;
};
CodeDigit::CodeDigit(not_null<Ui::RpWidget*> widget)
: Ui::AbstractButton(widget)
, _shaker(this) {
setBorderColor(st::windowBgRipple);
}
void CodeDigit::setDigit(int digit) {
if ((_dataDigit == digit) && _animation.animating()) {
return;
}
_dataDigit = digit;
if (_viewDigit != digit) {
_animation.stop();
if (digit == kDigitNone) {
_animation.start([=](float64 value) {
update();
if (!value) {
_viewDigit = digit;
}
}, 1., 0., st::universalDuration);
} else {
_viewDigit = digit;
_animation.start([=] { update(); }, 0, 1., st::universalDuration);
}
}
}
int CodeDigit::digit() const {
return _dataDigit;
}
void CodeDigit::setBorderColor(const QBrush &brush) {
_borderPen = QPen(brush, st::introCodeDigitBorderWidth);
update();
}
void CodeDigit::shake() {
_shaker.shake();
}
void CodeDigit::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto clipPath = QPainterPath();
clipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius);
p.setClipPath(clipPath);
p.fillRect(rect(), st::windowBgOver);
{
auto hq = PainterHighQualityEnabler(p);
p.strokePath(clipPath, _borderPen);
}
if (_viewDigit == kDigitNone) {
return;
}
const auto hiding = (_dataDigit == kDigitNone);
const auto progress = _animation.value(1.);
if (hiding) {
p.setOpacity(progress * progress);
const auto center = rect().center();
p.setTransform(QTransform()
.translate(center.x(), center.y())
.scale(progress, progress)
.translate(-center.x(), -center.y()));
} else {
p.setOpacity(progress);
constexpr auto kSlideDistanceRatio = 0.2;
const auto distance = rect().height() * kSlideDistanceRatio;
p.translate(0, (distance * (1. - progress)));
}
p.setFont(st::introCodeDigitFont);
p.setPen(st::windowFg);
p.drawText(rect(), QString::number(_viewDigit), style::al_center);
}
CodeInput::CodeInput(QWidget *parent)
: Ui::RpWidget(parent) {
setFocusPolicy(Qt::StrongFocus);
}
void CodeInput::setDigitsCountMax(int digitsCount) {
_digitsCountMax = digitsCount;
_digits.clear();
_currentIndex = 0;
constexpr auto kWidthRatio = 0.8;
const auto digitWidth = st::introCodeDigitHeight * kWidthRatio;
const auto padding = Margins(st::introCodeDigitSkip);
resize(
padding.left()
+ digitWidth * digitsCount
+ st::introCodeDigitSkip * (digitsCount - 1)
+ padding.right(),
st::introCodeDigitHeight);
for (auto i = 0; i < digitsCount; i++) {
const auto widget = Ui::CreateChild<CodeDigit>(this);
widget->setPointerCursor(false);
widget->setClickedCallback([=] { unfocusAll(_currentIndex = i); });
widget->resize(digitWidth, st::introCodeDigitHeight);
widget->moveToLeft(
padding.left() + (digitWidth + st::introCodeDigitSkip) * i,
0);
_digits.emplace_back(widget);
}
}
void CodeInput::setCode(QString code) {
using namespace TextUtilities;
code = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax);
for (int i = 0; i < _digits.size(); i++) {
if (i >= code.size()) {
return;
}
_digits[i]->setDigit(code.at(i).digitValue());
}
}
void CodeInput::requestCode() {
const auto result = collectDigits();
if (result.size() == _digitsCountMax) {
_codeCollected.fire_copy(result);
} else {
findEmptyAndPerform([&](int i) { _digits[i]->shake(); });
}
}
rpl::producer<QString> CodeInput::codeCollected() const {
return _codeCollected.events();
}
void CodeInput::clear() {
for (const auto &digit : _digits) {
digit->setDigit(kDigitNone);
}
unfocusAll(_currentIndex = 0);
}
void CodeInput::showError() {
clear();
for (const auto &digit : _digits) {
digit->shake();
digit->setBorderColor(st::activeLineFgError);
}
}
void CodeInput::focusInEvent(QFocusEvent *e) {
unfocusAll(_currentIndex);
}
void CodeInput::focusOutEvent(QFocusEvent *e) {
unfocusAll(kDigitNone);
}
void CodeInput::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
p.fillRect(rect(), st::windowBg);
}
void CodeInput::keyPressEvent(QKeyEvent *e) {
const auto key = e->key();
if (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) {
_currentIndex = Circular(_currentIndex + 1, _digits.size());
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Up || key == Qt::Key_Left) {
_currentIndex = Circular(_currentIndex - 1, _digits.size());
unfocusAll(_currentIndex);
} else if (key >= Qt::Key_0 && key <= Qt::Key_9) {
const auto index = int(key - Qt::Key_0);
_digits[_currentIndex]->setDigit(index);
_currentIndex = Circular(_currentIndex + 1, _digits.size());
if (!_currentIndex) {
const auto result = collectDigits();
if (result.size() == _digitsCountMax) {
_codeCollected.fire_copy(result);
_currentIndex = _digits.size() - 1;
} else {
findEmptyAndPerform([&](int i) { _currentIndex = i; });
}
}
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Delete) {
_digits[_currentIndex]->setDigit(kDigitNone);
} else if (key == Qt::Key_Backspace) {
const auto wasDigit = _digits[_currentIndex]->digit();
_digits[_currentIndex]->setDigit(kDigitNone);
_currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size()));
if (wasDigit == kDigitNone) {
_digits[_currentIndex]->setDigit(kDigitNone);
}
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
requestCode();
} else if (e == QKeySequence::Paste) {
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
} else if (key >= Qt::Key_A && key <= Qt::Key_Z) {
_digits[_currentIndex]->shake();
} else if (key == Qt::Key_Home || key == Qt::Key_PageUp) {
unfocusAll(_currentIndex = 0);
} else if (key == Qt::Key_End || key == Qt::Key_PageDown) {
unfocusAll(_currentIndex = (_digits.size() - 1));
}
}
void CodeInput::contextMenuEvent(QContextMenuEvent *e) {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::defaultPopupMenu);
_menu->addAction(tr::lng_mac_menu_paste(tr::now), [=] {
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
})->setEnabled(!QGuiApplication::clipboard()->text().isEmpty());
_menu->popup(QCursor::pos());
}
void CodeInput::insertCodeAndSubmit(const QString &code) {
if (code.isEmpty()) {
return;
}
setCode(code);
_currentIndex = _digits.size() - 1;
findEmptyAndPerform([&](int i) { _currentIndex = i; });
unfocusAll(_currentIndex);
if ((_currentIndex == _digits.size() - 1)
&& _digits[_currentIndex]->digit() != kDigitNone) {
requestCode();
}
}
QString CodeInput::collectDigits() const {
auto result = QString();
for (const auto &digit : _digits) {
if (digit->digit() != kDigitNone) {
result += QString::number(digit->digit());
}
}
return result;
}
void CodeInput::unfocusAll(int except) {
for (auto i = 0; i < _digits.size(); i++) {
const auto focused = (i == except);
_digits[i]->setBorderColor(focused
? st::windowActiveTextFg
: st::windowBgRipple);
}
}
void CodeInput::findEmptyAndPerform(const Fn<void(int)> &callback) {
for (auto i = 0; i < _digits.size(); i++) {
if (_digits[i]->digit() == kDigitNone) {
callback(i);
break;
}
}
}
} // namespace Ui