Improved style of input field for login code.

This commit is contained in:
23rd 2023-11-27 20:08:59 +03:00 committed by John Preston
parent 9ef0e5cf83
commit ac8117a6d8
6 changed files with 389 additions and 93 deletions

View File

@ -115,6 +115,12 @@ introPassword: introCountry;
introPasswordTop: 74px;
introPasswordHintTop: 151px;
introCodeDigitFont: font(20px);
introCodeDigitHeight: 50px;
introCodeDigitBorderWidth: 4px;
introCodeDigitSkip: 10px;
introCodeDigitAnimatioDuration: 120;
introPasswordHint: FlatLabel(introDescription) {
textFg: windowFg;
}

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "intro/intro_code.h"
#include "lang/lang_keys.h"
#include "intro/intro_code_input.h"
#include "intro/intro_signup.h"
#include "intro/intro_password_check.h"
#include "boxes/abstract_box.h"
@ -26,67 +27,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Intro {
namespace details {
CodeInput::CodeInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder)
: Ui::MaskedInputField(parent, st, std::move(placeholder)) {
}
void CodeInput::setDigitsCountMax(int digitsCount) {
_digitsCountMax = digitsCount;
}
void CodeInput::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) {
QString newText;
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0;
for (int i = 0; i < oldLen; ++i) {
if (now[i].isDigit()) {
++digitCount;
}
}
accumulate_min(digitCount, _digitsCountMax);
auto strict = (digitCount == _digitsCountMax);
newText.reserve(oldLen);
for (int i = 0; i < oldLen; ++i) {
QChar ch(now[i]);
if (ch.isDigit()) {
if (!digitCount--) {
break;
}
newText += ch;
if (strict && !digitCount) {
break;
}
} else if (ch == '-') {
newText += ch;
}
if (i == oldPos) {
newPos = newText.length();
}
}
if (newPos < 0 || newPos > newText.size()) {
newPos = newText.size();
}
if (newText != now) {
now = newText;
setText(now);
startPlaceholderAnimation();
}
if (newPos != nowCursor) {
nowCursor = newPos;
setCursorPosition(nowCursor);
}
}
CodeWidget::CodeWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _noTelegramCode(this, tr::lng_code_no_telegram(tr::now), st::introLink)
, _code(this, st::introCode, tr::lng_code_ph())
, _code(this)
, _callTimer([=] { sendCall(); })
, _callStatus(getData()->callStatus)
, _callTimeout(getData()->callTimeout)
@ -97,7 +44,6 @@ CodeWidget::CodeWidget(
refreshLang();
}, lifetime());
connect(_code, &CodeInput::changed, [=] { codeChanged(); });
_noTelegramCode->addClickHandler([=] { noTelegramCode(); });
_code->setDigitsCountMax(getData()->codeLength);
@ -111,9 +57,15 @@ CodeWidget::CodeWidget(
}) | rpl::flatten_latest());
account->setHandleLoginCode([=](const QString &code) {
_code->setText(code);
submitCode();
_code->setCode(code);
_code->requestCode();
});
_code->codeCollected(
) | rpl::start_with_next([=](const QString &code) {
hideError();
submitCode(code);
}, lifetime());
}
void CodeWidget::refreshLang() {
@ -210,7 +162,7 @@ void CodeWidget::showCodeError(rpl::producer<QString> text) {
}
void CodeWidget::setInnerFocus() {
_code->setFocusFast();
_code->setFocus();
}
void CodeWidget::activate() {
@ -233,7 +185,7 @@ void CodeWidget::finished() {
cancelled();
_sentCode.clear();
_code->setText(QString());
_code->clear();
}
void CodeWidget::cancelled() {
@ -267,6 +219,7 @@ void CodeWidget::checkRequest() {
void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) {
stopCheck();
_code->setEnabled(true);
_sentRequest = 0;
finish(result);
}
@ -274,12 +227,16 @@ void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) {
void CodeWidget::codeSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
stopCheck();
_code->setEnabled(true);
_code->setFocus();
_sentRequest = 0;
showCodeError(tr::lng_flood_error());
return;
}
stopCheck();
_code->setEnabled(true);
_code->setFocus();
_sentRequest = 0;
auto &err = error.type();
if (err == u"PHONE_NUMBER_INVALID"_q
@ -303,11 +260,6 @@ void CodeWidget::codeSubmitFail(const MTP::Error &error) {
}
}
void CodeWidget::codeChanged() {
hideError();
submitCode();
}
void CodeWidget::sendCall() {
if (_callStatus == CallStatus::Waiting) {
if (--_callTimeout <= 0) {
@ -370,19 +322,13 @@ void CodeWidget::gotPassword(const MTPaccount_Password &result) {
void CodeWidget::submit() {
if (getData()->codeByFragmentUrl.isEmpty()) {
submitCode();
_code->requestCode();
} else {
File::OpenUrl(getData()->codeByFragmentUrl);
}
}
void CodeWidget::submitCode() {
const auto text = QString(
_code->getLastText()
).remove(
TextUtilities::RegExpDigitsExclude()
).mid(0, getData()->codeLength);
void CodeWidget::submitCode(const QString &text) {
if (_sentRequest
|| _sentCode == text
|| text.size() != getData()->codeLength) {
@ -394,6 +340,7 @@ void CodeWidget::submitCode() {
_checkRequestTimer.callEach(1000);
_sentCode = text;
_code->setEnabled(false);
getData()->pwdState = Core::CloudPasswordState();
_sentRequest = api().request(MTPauth_SignIn(
MTP_flags(MTPauth_SignIn::Flag::f_phone_code),

View File

@ -16,6 +16,7 @@ namespace Ui {
class RoundButton;
class LinkButton;
class FlatLabel;
class CodeInput;
} // namespace Ui
namespace Intro {
@ -23,23 +24,6 @@ namespace details {
enum class CallStatus;
class CodeInput final : public Ui::MaskedInputField {
public:
CodeInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder);
void setDigitsCountMax(int digitsCount);
protected:
void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override;
private:
int _digitsCountMax = 5;
};
class CodeWidget final : public Step {
public:
CodeWidget(
@ -65,7 +49,6 @@ protected:
private:
void noTelegramCode();
void codeChanged();
void sendCall();
void checkRequest();
@ -85,14 +68,14 @@ private:
void noTelegramCodeDone(const MTPauth_SentCode &result);
void noTelegramCodeFail(const MTP::Error &result);
void submitCode();
void submitCode(const QString &text);
void stopCheck();
object_ptr<Ui::LinkButton> _noTelegramCode;
mtpRequestId _noTelegramCodeRequestId = 0;
object_ptr<CodeInput> _code;
object_ptr<Ui::CodeInput> _code;
QString _sentCode;
mtpRequestId _sentRequest = 0;

View File

@ -0,0 +1,308 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "intro/intro_code_input.h"
#include "ui/abstract_button.h"
#include "ui/effects/shake_animation.h"
#include "ui/rect.h"
#include "ui/text/text_entity.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h" // boxRadius
#include <QtCore/QRegularExpression>
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) {
constexpr auto kDuration = st::introCodeDigitAnimatioDuration;
_animation.stop();
if (digit == kDigitNone) {
_animation.start([=](float64 value) {
update();
if (!value) {
_viewDigit = digit;
}
}, 1., 0., kDuration);
} else {
_viewDigit = digit;
_animation.start([=] { update(); }, 0., 1., kDuration);
}
}
}
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);
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 (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));
}
}
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

View File

@ -0,0 +1,49 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/rp_widget.h"
namespace Ui {
class CodeDigit;
class CodeInput final : public Ui::RpWidget {
public:
CodeInput(QWidget *parent);
void setDigitsCountMax(int digitsCount);
void setCode(QString code);
void requestCode();
[[nodiscard]] rpl::producer<QString> codeCollected() const;
void clear();
void showError();
protected:
void focusInEvent(QFocusEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private:
[[nodiscard]] QString collectDigits() const;
void unfocusAll(int except);
void findEmptyAndPerform(const Fn<void(int)> &callback);
int _digitsCountMax = 0;
std::vector<not_null<CodeDigit*>> _digits;
int _currentIndex = 0;
rpl::event_stream<QString> _codeCollected;
};
} // namespace Ui

View File

@ -117,6 +117,9 @@ PRIVATE
info/boosts/giveaway/select_countries_box.cpp
info/boosts/giveaway/select_countries_box.h
intro/intro_code_input.cpp
intro/intro_code_input.h
layout/abstract_layout_item.cpp
layout/abstract_layout_item.h
layout/layout_mosaic.cpp