Allow date edition in passport.

This commit is contained in:
John Preston 2018-04-10 20:22:27 +04:00
parent e4e05a14b7
commit 9903546a2d
7 changed files with 549 additions and 6 deletions

View File

@ -1502,6 +1502,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_terms_decline" = "Decline";
"lng_terms_signup_sorry" = "We're very sorry, but this means you can't sign up for Telegram.\n\nUnlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data in Privacy & Security settings.\n\nBut if you're generally not OK with Telegram's modest needs, it won't be possible for us to provide this service.";
"lng_date_input_day" = "Day";
"lng_date_input_month" = "Month";
"lng_date_input_year" = "Year";
"lng_passport_title" = "Telegram passport";
"lng_passport_request1" = "{bot} requests access to your personal data";
"lng_passport_request2" = "to sign you up for their services";

View File

@ -185,14 +185,30 @@ passportDetailsPadding: margins(22px, 10px, 28px, 10px);
passportDetailsField: InputField(defaultInputField) {
textMargins: margins(2px, 8px, 2px, 0px);
placeholderScale: 0.;
placeholderFont: normalFont;
heightMin: 32px;
font: normalFont;
}
passportDetailsDateField: InputField(passportDetailsField) {
border: 0px;
borderActive: 0px;
heightMin: 30px;
placeholderFont: font(semibold 14px);
placeholderFgActive: placeholderFgActive;
}
passportDetailsSeparator: FlatLabel(passportPasswordLabelBold) {
style: TextStyle(defaultTextStyle) {
font: font(semibold 14px);
}
textFg: windowSubTextFg;
}
passportDetailsSeparatorPadding: margins(5px, 8px, 5px, 0px);
passportContactField: InputField(defaultInputField) {
font: normalFont;
}
passportDetailsFieldLeft: 116px;
passportDetailsFieldTop: 2px;
passportDetailsFieldSkipMin: 12px;
passportDetailsSkip: 30px;
passportRequestTypeSkip: 16px;

View File

@ -785,7 +785,7 @@ void PanelController::cancelEditScope() {
[=] {
_panel->showForm();
base::take(_confirmForgetChangesBox);
})).data());
})).data());
}
} else {
_panel->showForm();

View File

@ -68,9 +68,80 @@ private:
};
class DateRow : public TextRow {
class DateInput final : public Ui::MaskedInputField {
public:
using TextRow::TextRow;
using MaskedInputField::MaskedInputField;
void setMaxValue(int value);
rpl::producer<> erasePrevious() const;
rpl::producer<QChar> putNext() const;
protected:
void keyPressEvent(QKeyEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
int _maxValue = 0;
int _maxDigits = 0;
rpl::event_stream<> _erasePrevious;
rpl::event_stream<QChar> _putNext;
};
class DateRow : public PanelDetailsRow {
public:
DateRow(QWidget *parent, const QString &label, const QString &value);
bool setFocusFast() override;
rpl::producer<QString> value() const override;
QString valueCurrent() const override;
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
private:
void setInnerFocus();
void putNext(const object_ptr<DateInput> &field, QChar ch);
void erasePrevious(const object_ptr<DateInput> &field);
int resizeInner(int left, int top, int width) override;
void showInnerError() override;
void finishInnerAnimating() override;
void setErrorShown(bool error);
void setFocused(bool focused);
void startBorderAnimation();
template <typename Widget>
bool insideSeparator(QPoint position, const Widget &widget) const;
int day() const;
int month() const;
int year() const;
int number(const object_ptr<DateInput> &field) const;
object_ptr<DateInput> _day;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator1;
object_ptr<DateInput> _month;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator2;
object_ptr<DateInput> _year;
rpl::variable<QString> _value;
style::cursor _cursor = style::cur_default;
Animation _a_borderShown;
int _borderAnimationStart = 0;
Animation _a_borderOpacity;
bool _borderVisible = false;
Animation _a_error;
bool _error = false;
Animation _a_focused;
bool _focused = false;
};
@ -207,6 +278,438 @@ void CountryRow::chooseCountry() {
});
}
QDate ValidateDate(const QString &value) {
const auto match = QRegularExpression(
"^([0-9]{2})\\.([0-9]{2})\\.([0-9]{4})$").match(value);
if (!match.hasMatch()) {
return QDate();
}
auto result = QDate();
const auto readInt = [](const QString &value) {
auto ref = value.midRef(0);
while (!ref.isEmpty() && ref.at(0) == '0') {
ref = ref.mid(1);
}
return ref.toInt();
};
result.setDate(
readInt(match.captured(3)),
readInt(match.captured(2)),
readInt(match.captured(1)));
return result;
}
QString GetDay(const QString &value) {
if (const auto date = ValidateDate(value); date.isValid()) {
return QString("%1").arg(date.day(), 2, 10, QChar('0'));
}
return QString();
}
QString GetMonth(const QString &value) {
if (const auto date = ValidateDate(value); date.isValid()) {
return QString("%1").arg(date.month(), 2, 10, QChar('0'));
}
return QString();
}
QString GetYear(const QString &value) {
if (const auto date = ValidateDate(value); date.isValid()) {
return QString("%1").arg(date.year(), 4, 10, QChar('0'));
}
return QString();
}
void DateInput::setMaxValue(int value) {
_maxValue = value;
_maxDigits = 0;
while (value > 0) {
++_maxDigits;
value /= 10;
}
}
rpl::producer<> DateInput::erasePrevious() const {
return _erasePrevious.events();
}
rpl::producer<QChar> DateInput::putNext() const {
return _putNext.events();
}
void DateInput::keyPressEvent(QKeyEvent *e) {
const auto isBackspace = (e->key() == Qt::Key_Backspace);
const auto isBeginning = (cursorPosition() == 0);
if (isBackspace && isBeginning) {
_erasePrevious.fire({});
} else {
MaskedInputField::keyPressEvent(e);
}
}
void DateInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
auto newText = QString();
auto newCursor = -1;
const auto oldCursor = nowCursor;
const auto oldLength = now.size();
auto accumulated = 0;
auto limit = 0;
for (; limit != oldLength; ++limit) {
if (now[limit].isDigit()) {
accumulated *= 10;
accumulated += (now[limit].unicode() - '0');
if (accumulated > _maxValue || limit == _maxDigits) {
break;
}
}
}
for (auto i = 0; i != limit;) {
if (now[i].isDigit()) {
newText += now[i];
}
if (++i == oldCursor) {
newCursor = newText.size();
}
}
if (newCursor < 0) {
newCursor = newText.size();
}
if (newText != now) {
now = newText;
setText(now);
startPlaceholderAnimation();
}
if (newCursor != nowCursor) {
nowCursor = newCursor;
setCursorPosition(nowCursor);
}
if (accumulated > _maxValue
|| (limit == _maxDigits && oldLength > _maxDigits)) {
if (oldCursor > limit) {
_putNext.fire('0' + (accumulated % 10));
} else {
_putNext.fire(0);
}
}
}
DateRow::DateRow(
QWidget *parent,
const QString &label,
const QString &value)
: PanelDetailsRow(parent, label)
, _day(
this,
st::passportDetailsDateField,
langFactory(lng_date_input_day),
GetDay(value))
, _separator1(
this,
object_ptr<Ui::FlatLabel>(
this,
QString(" / "),
Ui::FlatLabel::InitType::Simple,
st::passportDetailsSeparator),
st::passportDetailsSeparatorPadding)
, _month(
this,
st::passportDetailsDateField,
langFactory(lng_date_input_month),
GetMonth(value))
, _separator2(
this,
object_ptr<Ui::FlatLabel>(
this,
QString(" / "),
Ui::FlatLabel::InitType::Simple,
st::passportDetailsSeparator),
st::passportDetailsSeparatorPadding)
, _year(
this,
st::passportDetailsDateField,
langFactory(lng_date_input_year),
GetYear(value))
, _value(valueCurrent()) {
const auto focused = [=](const object_ptr<DateInput> &field) {
return [this, pointer = make_weak(field.data())]{
_borderAnimationStart = pointer->borderAnimationStart()
+ pointer->x()
- _day->x();
setFocused(true);
};
};
const auto blurred = [=] {
setFocused(false);
};
connect(_day, &Ui::MaskedInputField::focused, focused(_day));
connect(_month, &Ui::MaskedInputField::focused, focused(_month));
connect(_year, &Ui::MaskedInputField::focused, focused(_year));
connect(_day, &Ui::MaskedInputField::blurred, blurred);
connect(_month, &Ui::MaskedInputField::blurred, blurred);
connect(_year, &Ui::MaskedInputField::blurred, blurred);
_day->setMaxValue(31);
_day->putNext() | rpl::start_with_next([=](QChar ch) {
putNext(_month, ch);
}, lifetime());
_month->setMaxValue(12);
_month->putNext() | rpl::start_with_next([=](QChar ch) {
putNext(_year, ch);
}, lifetime());
_month->erasePrevious() | rpl::start_with_next([=] {
erasePrevious(_day);
}, lifetime());
_year->setMaxValue(2999);
_year->erasePrevious() | rpl::start_with_next([=] {
erasePrevious(_month);
}, lifetime());
_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
_separator2->setAttribute(Qt::WA_TransparentForMouseEvents);
setMouseTracking(true);
}
void DateRow::putNext(const object_ptr<DateInput> &field, QChar ch) {
field->setCursorPosition(0);
if (ch.unicode()) {
field->setText(ch + field->getLastText());
field->setCursorPosition(1);
}
field->setFocus();
}
void DateRow::erasePrevious(const object_ptr<DateInput> &field) {
const auto text = field->getLastText();
if (!text.isEmpty()) {
field->setCursorPosition(text.size() - 1);
field->setText(text.mid(0, text.size() - 1));
}
field->setFocus();
}
bool DateRow::setFocusFast() {
if (day()) {
if (month()) {
_year->setFocusFast();
} else {
_month->setFocusFast();
}
} else {
_day->setFocusFast();
}
return true;
}
int DateRow::number(const object_ptr<DateInput> &field) const {
const auto text = field->getLastText();
auto ref = text.midRef(0);
while (!ref.isEmpty() && ref.at(0) == '0') {
ref = ref.mid(1);
}
return ref.toInt();
}
int DateRow::day() const {
return number(_day);
}
int DateRow::month() const {
return number(_month);
}
int DateRow::year() const {
return number(_year);
}
QString DateRow::valueCurrent() const {
const auto result = QString("%1.%2.%3"
).arg(day(), 2, 10, QChar('0')
).arg(month(), 2, 10, QChar('0')
).arg(year(), 4, 10, QChar('0'));
return ValidateDate(result).isValid() ? result : QString();
}
rpl::producer<QString> DateRow::value() const {
return _value.value();
}
void DateRow::paintEvent(QPaintEvent *e) {
PanelDetailsRow::paintEvent(e);
Painter p(this);
const auto &_st = st::passportDetailsField;
const auto height = _st.heightMin;
const auto width = _year->x() + _year->width() - _day->x();
p.translate(_day->x(), _day->y());
if (_st.border) {
p.fillRect(0, height - _st.border, width, _st.border, _st.borderFg);
}
const auto ms = getms();
auto errorDegree = _a_error.current(ms, _error ? 1. : 0.);
auto focusedDegree = _a_focused.current(ms, _focused ? 1. : 0.);
auto borderShownDegree = _a_borderShown.current(ms, 1.);
auto borderOpacity = _a_borderOpacity.current(ms, _borderVisible ? 1. : 0.);
if (_st.borderActive && (borderOpacity > 0.)) {
auto borderStart = snap(_borderAnimationStart, 0, width);
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
auto borderTo = borderStart + qRound((width - borderStart) * borderShownDegree);
if (borderTo > borderFrom) {
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
p.setOpacity(borderOpacity);
p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
p.setOpacity(1);
}
}
}
template <typename Widget>
bool DateRow::insideSeparator(QPoint position, const Widget &widget) const {
const auto x = position.x();
const auto y = position.y();
return (x >= widget->x() && x < widget->x() + widget->width())
&& (y >= _day->y() && y < _day->y() + _day->height());
}
void DateRow::mouseMoveEvent(QMouseEvent *e) {
const auto cursor = (insideSeparator(e->pos(), _separator1)
|| insideSeparator(e->pos(), _separator2))
? style::cur_text
: style::cur_default;
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
void DateRow::mousePressEvent(QMouseEvent *e) {
const auto x = e->pos().x();
const auto focus1 = [&] {
if (_day->getLastText().size() > 1) {
_month->setFocus();
} else {
_day->setFocus();
}
};
if (insideSeparator(e->pos(), _separator1)) {
focus1();
_borderAnimationStart = x - _day->x();
} else if (insideSeparator(e->pos(), _separator2)) {
if (_month->getLastText().size() > 1) {
_year->setFocus();
} else {
focus1();
}
_borderAnimationStart = x - _day->x();
}
}
int DateRow::resizeInner(int left, int top, int width) {
const auto right = left + width;
const auto &_st = st::passportDetailsDateField;
const auto &font = _st.placeholderFont;
const auto dayWidth = _st.textMargins.left()
+ _st.placeholderMargins.left()
+ font->width(lang(lng_date_input_day))
+ _st.placeholderMargins.right()
+ _st.textMargins.right()
+ st::lineWidth;
const auto monthWidth = _st.textMargins.left()
+ _st.placeholderMargins.left()
+ font->width(lang(lng_date_input_month))
+ _st.placeholderMargins.right()
+ _st.textMargins.right()
+ st::lineWidth;
_day->setGeometry(left, top, dayWidth, _day->height());
left += dayWidth - st::lineWidth;
_separator1->resizeToNaturalWidth(width);
_separator1->move(left, top);
left += _separator1->width();
_month->setGeometry(left, top, monthWidth, _month->height());
left += monthWidth - st::lineWidth;
_separator2->resizeToNaturalWidth(width);
_separator2->move(left, top);
left += _separator2->width();
_year->setGeometry(left, top, right - left, _year->height());
return st::semiboldFont->height;
}
void DateRow::showInnerError() {
setErrorShown(true);
if (_year->getLastText().size() == 2) {
// We don't support year 95 for 1995 or 03 for 2003.
// Let's give a hint to our user what is wrong.
_year->setFocus();
_year->selectAll();
} else if (!_focused) {
setInnerFocus();
}
}
void DateRow::setInnerFocus() {
if (day()) {
if (month()) {
_year->setFocus();
} else {
_month->setFocus();
}
} else {
_day->setFocus();
}
}
void DateRow::setErrorShown(bool error) {
if (_error != error) {
_error = error;
_a_error.start(
[=] { update(); },
_error ? 0. : 1.,
_error ? 1. : 0.,
st::passportDetailsField.duration);
startBorderAnimation();
}
}
void DateRow::setFocused(bool focused) {
if (_focused != focused) {
_focused = focused;
_a_focused.start(
[=] { update(); },
_focused ? 0. : 1.,
_focused ? 1. : 0.,
st::passportDetailsField.duration);
startBorderAnimation();
}
}
void DateRow::finishInnerAnimating() {
_day->finishAnimating();
_month->finishAnimating();
_year->finishAnimating();
_a_borderOpacity.finish();
_a_borderShown.finish();
_a_error.finish();
}
void DateRow::startBorderAnimation() {
auto borderVisible = (_error || _focused);
if (_borderVisible != borderVisible) {
_borderVisible = borderVisible;
const auto duration = st::passportDetailsField.duration;
if (_borderVisible) {
if (_a_borderOpacity.animating()) {
_a_borderOpacity.start([=] { update(); }, 0., 1., duration);
} else {
_a_borderShown.start([=] { update(); }, 0., 1., duration);
}
} else {
_a_borderOpacity.start([=] { update(); }, 1., 0., duration);
}
}
}
} // namespace
int PanelLabel::naturalWidth() const {

View File

@ -240,6 +240,9 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
QString())));
}
inner->add(
object_ptr<Ui::FixedHeightWidget>(inner, st::passportDetailsSkip));
return inner;
}

View File

@ -1530,7 +1530,11 @@ void InputField::paintEvent(QPaintEvent *e) {
auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
p.setFont(_st.font);
QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins));
r.moveLeft(r.left() + placeholderLeft);
if (rtl()) r.moveLeft(width() - r.left() - r.width());
p.setFont(_st.placeholderFont);
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
if (_st.placeholderAlign == style::al_topleft && _placeholderAfterSymbols > 0) {
@ -1581,7 +1585,9 @@ void InputField::startBorderAnimation() {
}
void InputField::focusInEvent(QFocusEvent *e) {
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2);
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason)
? mapFromGlobal(QCursor::pos()).x()
: (width() / 2);
QTimer::singleShot(0, this, SLOT(onFocusInner()));
}
@ -1596,6 +1602,10 @@ void InputField::onFocusInner() {
_borderAnimationStart = borderStart;
}
int InputField::borderAnimationStart() const {
return _borderAnimationStart;
}
void InputField::contextMenuEvent(QContextMenuEvent *e) {
_inner->contextMenuEvent(e);
}
@ -3374,6 +3384,10 @@ void MaskedInputField::customUpDown(bool custom) {
_customUpDown = custom;
}
int MaskedInputField::borderAnimationStart() const {
return _borderAnimationStart;
}
void MaskedInputField::setTextMargins(const QMargins &mrg) {
_textMargins = mrg;
refreshPlaceholder();
@ -3505,7 +3519,7 @@ void MaskedInputField::paintEvent(QPaintEvent *e) {
r.moveLeft(r.left() + placeholderLeft);
if (rtl()) r.moveLeft(width() - r.left() - r.width());
p.setFont(_st.font);
p.setFont(_st.placeholderFont);
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
p.drawText(r, _placeholder, _st.placeholderAlign);

View File

@ -261,6 +261,7 @@ public:
void setSubmitSettings(SubmitSettings settings);
void customUpDown(bool isCustom);
void customTab(bool isCustom);
int borderAnimationStart() const;
not_null<QTextDocument*> document();
not_null<const QTextDocument*> document() const;
@ -493,6 +494,8 @@ public:
QSize minimumSizeHint() const override;
void customUpDown(bool isCustom);
int borderAnimationStart() const;
const QString &getLastText() const {
return _oldtext;
}