mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 06:54:43 +00:00
Allow date edition in passport.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -785,7 +785,7 @@ void PanelController::cancelEditScope() {
|
||||
[=] {
|
||||
_panel->showForm();
|
||||
base::take(_confirmForgetChangesBox);
|
||||
})).data());
|
||||
})).data());
|
||||
}
|
||||
} else {
|
||||
_panel->showForm();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -240,6 +240,9 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
|
||||
QString())));
|
||||
}
|
||||
|
||||
inner->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(inner, st::passportDetailsSkip));
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user