| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719 |
- /*
- 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 "payments/ui/payments_field.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/boxes/country_select_box.h"
- #include "ui/text/format_values.h"
- #include "ui/ui_utility.h"
- #include "ui/widgets/fields/special_fields.h"
- #include "countries/countries_instance.h"
- #include "base/platform/base_platform_info.h"
- #include "base/event_filter.h"
- #include "base/qt/qt_common_adapters.h"
- #include "styles/style_payments.h"
- #include <QtCore/QRegularExpression>
- #include <QtWidgets/QTextEdit>
- namespace Payments::Ui {
- namespace {
- struct SimpleFieldState {
- QString value;
- int position = 0;
- };
- [[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {
- return (rule.thousands == '.' || rule.thousands == ',')
- ? ' '
- : rule.thousands;
- }
- [[nodiscard]] QString RemoveNonNumbers(QString value) {
- static const auto RegExp = QRegularExpression("[^0-9]");
- return value.replace(RegExp, QString());
- }
- [[nodiscard]] SimpleFieldState CleanMoneyState(
- const CurrencyRule &rule,
- SimpleFieldState state) {
- const auto withDecimal = state.value.replace(
- QChar('.'),
- rule.decimal
- ).replace(
- QChar(','),
- rule.decimal
- );
- const auto digitsLimit = 16 - rule.exponent;
- const auto beforePosition = state.value.mid(0, state.position);
- auto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));
- if (decimalPosition < 0) {
- state = {
- .value = RemoveNonNumbers(state.value),
- .position = int(RemoveNonNumbers(beforePosition).size()),
- };
- } else {
- const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
- state.value.mid(0, decimalPosition));
- state = {
- .value = (onlyNumbersBeforeDecimal
- + QChar(rule.decimal)
- + RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
- .position = int(RemoveNonNumbers(beforePosition).size()
- + (state.position > decimalPosition ? 1 : 0)),
- };
- decimalPosition = onlyNumbersBeforeDecimal.size();
- const auto maxLength = decimalPosition + 1 + rule.exponent;
- if (state.value.size() > maxLength) {
- state = {
- .value = state.value.mid(0, maxLength),
- .position = std::min(state.position, maxLength),
- };
- }
- }
- if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {
- state = {
- .value = QChar('0') + state.value,
- .position = state.position + 1,
- };
- if (decimalPosition >= 0) {
- ++decimalPosition;
- }
- }
- auto skip = 0;
- while (state.value.size() > skip + 1
- && state.value[skip] == QChar('0')
- && state.value[skip + 1] != QChar(rule.decimal)) {
- ++skip;
- }
- state = {
- .value = state.value.mid(skip),
- .position = std::max(state.position - skip, 0),
- };
- if (decimalPosition >= 0) {
- Assert(decimalPosition >= skip);
- decimalPosition -= skip;
- if (decimalPosition > digitsLimit) {
- state = {
- .value = (state.value.mid(0, digitsLimit)
- + state.value.mid(decimalPosition)),
- .position = (state.position > digitsLimit
- ? std::max(
- state.position - (decimalPosition - digitsLimit),
- digitsLimit)
- : state.position),
- };
- }
- } else if (state.value.size() > digitsLimit) {
- state = {
- .value = state.value.mid(0, digitsLimit),
- .position = std::min(state.position, digitsLimit),
- };
- }
- return state;
- }
- [[nodiscard]] SimpleFieldState PostprocessMoneyResult(
- const CurrencyRule &rule,
- SimpleFieldState result) {
- const auto position = result.value.indexOf(rule.decimal);
- const auto from = (position >= 0) ? position : result.value.size();
- for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {
- result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));
- if (result.position >= insertAt) {
- ++result.position;
- }
- }
- return result;
- }
- [[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
- return (request.wasAnchor == request.wasPosition)
- && (request.wasPosition == request.nowPosition + 1)
- && (base::StringViewMid(request.wasValue, 0, request.wasPosition - 1)
- == base::StringViewMid(request.nowValue, 0, request.nowPosition))
- && (base::StringViewMid(request.wasValue, request.wasPosition)
- == base::StringViewMid(request.nowValue, request.nowPosition));
- }
- [[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
- return (request.wasAnchor == request.wasPosition)
- && (request.wasPosition == request.nowPosition)
- && (base::StringViewMid(request.wasValue, 0, request.wasPosition)
- == base::StringViewMid(request.nowValue, 0, request.nowPosition))
- && (base::StringViewMid(request.wasValue, request.wasPosition + 1)
- == base::StringViewMid(request.nowValue, request.nowPosition));
- }
- [[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {
- return [=](FieldValidateRequest request) {
- const auto realNowState = [&] {
- const auto backspaced = IsBackspace(request);
- const auto deleted = IsDelete(request);
- if (!backspaced && !deleted) {
- return CleanMoneyState(rule, {
- .value = request.nowValue,
- .position = request.nowPosition,
- });
- }
- const auto realWasState = CleanMoneyState(rule, {
- .value = request.wasValue,
- .position = request.wasPosition,
- });
- const auto changedValue = deleted
- ? (realWasState.value.mid(0, realWasState.position)
- + realWasState.value.mid(realWasState.position + 1))
- : (realWasState.position > 1)
- ? (realWasState.value.mid(0, realWasState.position - 1)
- + realWasState.value.mid(realWasState.position))
- : realWasState.value.mid(realWasState.position);
- return SimpleFieldState{
- .value = changedValue,
- .position = (deleted
- ? realWasState.position
- : std::max(realWasState.position - 1, 0))
- };
- }();
- const auto postprocessed = PostprocessMoneyResult(
- rule,
- realNowState);
- return FieldValidateResult{
- .value = postprocessed.value,
- .position = postprocessed.position,
- };
- };
- }
- [[nodiscard]] QString Parse(const FieldConfig &config) {
- if (config.type == FieldType::Country) {
- return Countries::Instance().countryNameByISO2(config.value);
- } else if (config.type == FieldType::Money) {
- const auto amount = config.value.toLongLong();
- if (!amount) {
- return QString();
- }
- const auto rule = LookupCurrencyRule(config.currency);
- const auto value = std::abs(amount) / std::pow(10., rule.exponent);
- const auto precision = (!rule.stripDotZero
- || std::floor(value) != value)
- ? rule.exponent
- : 0;
- return FormatWithSeparators(
- value,
- precision,
- rule.decimal,
- FieldThousandsSeparator(rule));
- }
- return config.value;
- }
- [[nodiscard]] QString Format(
- const FieldConfig &config,
- const QString &parsed,
- const QString &countryIso2) {
- if (config.type == FieldType::Country) {
- return countryIso2;
- } else if (config.type == FieldType::Money) {
- static const auto RegExp = QRegularExpression("[^0-9\\.]");
- const auto rule = LookupCurrencyRule(config.currency);
- const auto real = QString(parsed).replace(
- QChar(rule.decimal),
- QChar('.')
- ).replace(
- QChar(','),
- QChar('.')
- ).replace(
- RegExp,
- QString()
- ).toDouble();
- return QString::number(
- int64(base::SafeRound(real * std::pow(10., rule.exponent))));
- } else if (config.type == FieldType::CardNumber
- || config.type == FieldType::CardCVC) {
- static const auto RegExp = QRegularExpression("[^0-9]");
- return QString(parsed).replace(RegExp, QString());
- }
- return parsed;
- }
- [[nodiscard]] bool UseMaskedField(FieldType type) {
- switch (type) {
- case FieldType::Text:
- case FieldType::Email:
- return false;
- case FieldType::CardNumber:
- case FieldType::CardExpireDate:
- case FieldType::CardCVC:
- case FieldType::Country:
- case FieldType::Phone:
- case FieldType::Money:
- return true;
- }
- Unexpected("FieldType in Payments::Ui::UseMaskedField.");
- }
- [[nodiscard]] base::unique_qptr<RpWidget> CreateWrap(
- QWidget *parent,
- FieldConfig &config) {
- switch (config.type) {
- case FieldType::Text:
- case FieldType::Email:
- return base::make_unique_q<InputField>(
- parent,
- st::paymentsField,
- std::move(config.placeholder),
- Parse(config));
- case FieldType::CardNumber:
- case FieldType::CardExpireDate:
- case FieldType::CardCVC:
- case FieldType::Country:
- case FieldType::Phone:
- case FieldType::Money:
- return base::make_unique_q<RpWidget>(parent);
- }
- Unexpected("FieldType in Payments::Ui::CreateWrap.");
- }
- [[nodiscard]] InputField *LookupInputField(
- not_null<RpWidget*> wrap,
- FieldConfig &config) {
- return UseMaskedField(config.type)
- ? nullptr
- : static_cast<InputField*>(wrap.get());
- }
- [[nodiscard]] MaskedInputField *CreateMoneyField(
- not_null<RpWidget*> wrap,
- FieldConfig &config,
- rpl::producer<> textPossiblyChanged) {
- struct State {
- CurrencyRule rule;
- style::InputField st;
- QString currencyText;
- int currencySkip = 0;
- FlatLabel *left = nullptr;
- FlatLabel *right = nullptr;
- };
- const auto state = wrap->lifetime().make_state<State>(State{
- .rule = LookupCurrencyRule(config.currency),
- .st = st::paymentsMoneyField,
- });
- const auto &rule = state->rule;
- state->currencySkip = rule.space ? state->st.style.font->spacew : 0;
- state->currencyText = ((!rule.left && rule.space)
- ? QString(QChar(' '))
- : QString()) + (*rule.international
- ? QString(rule.international)
- : config.currency) + ((rule.left && rule.space)
- ? QString(QChar(' '))
- : QString());
- if (rule.left) {
- state->left = CreateChild<FlatLabel>(
- wrap.get(),
- state->currencyText,
- st::paymentsFieldAdditional);
- }
- state->right = CreateChild<FlatLabel>(
- wrap.get(),
- QString(),
- st::paymentsFieldAdditional);
- const auto leftSkip = state->left
- ? (state->left->textMaxWidth() + state->currencySkip)
- : 0;
- const auto rightSkip = st::paymentsFieldAdditional.style.font->width(
- QString(QChar(rule.decimal))
- + QString(QChar('0')).repeated(rule.exponent)
- + (rule.left ? QString() : state->currencyText));
- state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);
- state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);
- const auto result = CreateChild<MaskedInputField>(
- wrap.get(),
- state->st,
- std::move(config.placeholder),
- Parse(config));
- result->setPlaceholderHidden(true);
- if (state->left) {
- state->left->move(0, state->st.textMargins.top());
- }
- const auto updateRight = [=] {
- const auto text = result->getLastText();
- const auto width = state->st.style.font->width(text);
- const auto &rule = state->rule;
- const auto symbol = QChar(rule.decimal);
- const auto decimal = text.indexOf(symbol);
- const auto zeros = (decimal >= 0)
- ? std::max(rule.exponent - int(text.size() - decimal - 1), 0)
- : rule.stripDotZero
- ? 0
- : rule.exponent;
- const auto valueDecimalSeparator = (decimal >= 0 || !zeros)
- ? QString()
- : QString(symbol);
- const auto zeroString = QString(QChar('0'));
- const auto valueRightPart = (text.isEmpty() ? zeroString : QString())
- + valueDecimalSeparator
- + zeroString.repeated(zeros);
- const auto right = valueRightPart
- + (rule.left ? QString() : state->currencyText);
- state->right->setText(right);
- state->right->setTextColorOverride(valueRightPart.isEmpty()
- ? std::nullopt
- : std::make_optional(st::windowSubTextFg->c));
- state->right->move(
- (state->st.textMargins.left()
- + width
- + ((rule.left || !valueRightPart.isEmpty())
- ? 0
- : state->currencySkip)),
- state->st.textMargins.top());
- };
- std::move(
- textPossiblyChanged
- ) | rpl::start_with_next(updateRight, result->lifetime());
- if (state->left) {
- state->left->raise();
- }
- state->right->raise();
- return result;
- }
- [[nodiscard]] MaskedInputField *LookupMaskedField(
- not_null<RpWidget*> wrap,
- FieldConfig &config,
- rpl::producer<> textPossiblyChanged) {
- if (!UseMaskedField(config.type)) {
- return nullptr;
- }
- switch (config.type) {
- case FieldType::Text:
- case FieldType::Email:
- return nullptr;
- case FieldType::CardNumber:
- case FieldType::CardExpireDate:
- case FieldType::CardCVC:
- case FieldType::Country:
- return CreateChild<MaskedInputField>(
- wrap.get(),
- st::paymentsField,
- std::move(config.placeholder),
- Parse(config));
- case FieldType::Phone:
- return CreateChild<PhoneInput>(
- wrap.get(),
- st::paymentsField,
- std::move(config.placeholder),
- Countries::ExtractPhoneCode(config.defaultPhone),
- Parse(config),
- [](const QString &s) { return Countries::Groups(s); });
- case FieldType::Money:
- return CreateMoneyField(
- wrap,
- config,
- std::move(textPossiblyChanged));
- }
- Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
- }
- } // namespace
- Field::Field(QWidget *parent, FieldConfig &&config)
- : _config(config)
- , _wrap(CreateWrap(parent, config))
- , _input(LookupInputField(_wrap.get(), config))
- , _masked(LookupMaskedField(
- _wrap.get(),
- config,
- _textPossiblyChanged.events_starting_with({})))
- , _countryIso2(config.value) {
- if (_masked) {
- setupMaskedGeometry();
- }
- if (_config.type == FieldType::Country) {
- setupCountry();
- }
- if (const auto &validator = config.validator) {
- setupValidator(validator);
- } else if (config.type == FieldType::Money) {
- setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
- }
- setupFrontBackspace();
- setupSubmit();
- }
- RpWidget *Field::widget() const {
- return _wrap.get();
- }
- object_ptr<RpWidget> Field::ownedWidget() const {
- return object_ptr<RpWidget>::fromRaw(_wrap.get());
- }
- QString Field::value() const {
- return Format(
- _config,
- _input ? _input->getLastText() : _masked->getLastText(),
- _countryIso2);
- }
- rpl::producer<> Field::frontBackspace() const {
- return _frontBackspace.events();
- }
- rpl::producer<> Field::finished() const {
- return _finished.events();
- }
- rpl::producer<> Field::submitted() const {
- return _submitted.events();
- }
- void Field::setupMaskedGeometry() {
- Expects(_masked != nullptr);
- _wrap->resize(_masked->size());
- _wrap->widthValue(
- ) | rpl::start_with_next([=](int width) {
- _masked->resize(width, _masked->height());
- }, _masked->lifetime());
- _masked->heightValue(
- ) | rpl::start_with_next([=](int height) {
- _wrap->resize(_wrap->width(), height);
- }, _masked->lifetime());
- }
- void Field::setupCountry() {
- Expects(_config.type == FieldType::Country);
- Expects(_masked != nullptr);
- QObject::connect(_masked, &MaskedInputField::focused, [=] {
- setFocus();
- const auto name = Countries::Instance().countryNameByISO2(
- _countryIso2);
- const auto country = !name.isEmpty()
- ? _countryIso2
- : !_config.defaultCountry.isEmpty()
- ? _config.defaultCountry
- : Platform::SystemCountry();
- auto box = Box<CountrySelectBox>(
- country,
- CountrySelectBox::Type::Countries);
- const auto raw = box.data();
- raw->countryChosen(
- ) | rpl::start_with_next([=](QString iso2) {
- _countryIso2 = iso2;
- _masked->setText(Countries::Instance().countryNameByISO2(iso2));
- _masked->hideError();
- raw->closeBox();
- if (!iso2.isEmpty()) {
- if (_nextField) {
- _nextField->activate();
- } else {
- _submitted.fire({});
- }
- }
- }, _masked->lifetime());
- raw->boxClosing() | rpl::start_with_next([=] {
- setFocus();
- }, _masked->lifetime());
- _config.showBox(std::move(box));
- });
- }
- void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
- Expects(validator != nullptr);
- const auto state = [=]() -> State {
- if (_masked) {
- const auto position = _masked->cursorPosition();
- const auto selectionStart = _masked->selectionStart();
- const auto selectionEnd = _masked->selectionEnd();
- return {
- .value = _masked->getLastText(),
- .position = position,
- .anchor = (selectionStart == selectionEnd
- ? position
- : (selectionStart == position)
- ? selectionEnd
- : selectionStart),
- };
- }
- const auto cursor = _input->textCursor();
- return {
- .value = _input->getLastText(),
- .position = cursor.position(),
- .anchor = cursor.anchor(),
- };
- };
- const auto save = [=] {
- _was = state();
- };
- const auto setText = [=](const QString &text) {
- if (_masked) {
- _masked->setText(text);
- } else {
- _input->setText(text);
- }
- };
- const auto setPosition = [=](int position) {
- if (_masked) {
- _masked->setCursorPosition(position);
- } else {
- auto cursor = _input->textCursor();
- cursor.setPosition(position);
- _input->setTextCursor(cursor);
- }
- };
- const auto validate = [=] {
- if (_validating) {
- return;
- }
- _validating = true;
- const auto guard = gsl::finally([&] {
- _validating = false;
- save();
- _textPossiblyChanged.fire({});
- });
- const auto now = state();
- const auto result = validator(ValidateRequest{
- .wasValue = _was.value,
- .wasPosition = _was.position,
- .wasAnchor = _was.anchor,
- .nowValue = now.value,
- .nowPosition = now.position,
- });
- _valid = result.finished || !result.invalid;
- const auto changed = (result.value != now.value);
- if (changed) {
- setText(result.value);
- }
- if (changed || result.position != now.position) {
- setPosition(result.position);
- }
- if (result.finished) {
- _finished.fire({});
- } else if (result.invalid) {
- Ui::PostponeCall(
- _masked ? (QWidget*)_masked : _input,
- [=] { showErrorNoFocus(); });
- }
- };
- if (_masked) {
- QObject::connect(_masked, &QLineEdit::cursorPositionChanged, save);
- QObject::connect(_masked, &MaskedInputField::changed, validate);
- } else {
- const auto raw = _input->rawTextEdit();
- QObject::connect(raw, &QTextEdit::cursorPositionChanged, save);
- _input->changes(
- ) | rpl::start_with_next(validate, _input->lifetime());
- }
- }
- void Field::setupFrontBackspace() {
- const auto filter = [=](not_null<QEvent*> e) {
- const auto frontBackspace = (e->type() == QEvent::KeyPress)
- && (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)
- && (_masked
- ? (_masked->cursorPosition() == 0
- && _masked->selectionLength() == 0)
- : (_input->textCursor().position() == 0
- && _input->textCursor().anchor() == 0));
- if (frontBackspace) {
- _frontBackspace.fire({});
- }
- return base::EventFilterResult::Continue;
- };
- if (_masked) {
- base::install_event_filter(_masked, filter);
- } else {
- base::install_event_filter(_input->rawTextEdit(), filter);
- }
- }
- void Field::setupSubmit() {
- const auto submitted = [=] {
- if (!_valid) {
- showError();
- } else if (_nextField) {
- _nextField->activate();
- } else {
- _submitted.fire({});
- }
- };
- if (_masked) {
- QObject::connect(_masked, &MaskedInputField::submitted, submitted);
- } else {
- _input->submits(
- ) | rpl::start_with_next(submitted, _input->lifetime());
- }
- }
- void Field::setNextField(not_null<Field*> field) {
- _nextField = field;
- finished() | rpl::start_with_next([=] {
- field->setFocus();
- }, _masked ? _masked->lifetime() : _input->lifetime());
- }
- void Field::setPreviousField(not_null<Field*> field) {
- frontBackspace(
- ) | rpl::start_with_next([=] {
- field->setFocus();
- }, _masked ? _masked->lifetime() : _input->lifetime());
- }
- void Field::activate() {
- if (_input) {
- _input->setFocus();
- } else {
- _masked->setFocus();
- }
- }
- void Field::setFocus() {
- if (_config.type == FieldType::Country) {
- _wrap->setFocus();
- } else {
- activate();
- }
- }
- void Field::setFocusFast() {
- if (_config.type == FieldType::Country) {
- setFocus();
- } else if (_input) {
- _input->setFocusFast();
- } else {
- _masked->setFocusFast();
- }
- }
- void Field::showError() {
- if (_config.type == FieldType::Country) {
- setFocus();
- _masked->showErrorNoFocus();
- } else if (_input) {
- _input->showError();
- } else {
- _masked->showError();
- }
- }
- void Field::showErrorNoFocus() {
- if (_input) {
- _input->showErrorNoFocus();
- } else {
- _masked->showErrorNoFocus();
- }
- }
- } // namespace Payments::Ui
|