2021-03-26 15:23:12 +00:00
|
|
|
/*
|
|
|
|
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/input_fields.h"
|
2021-03-26 17:09:09 +00:00
|
|
|
#include "ui/boxes/country_select_box.h"
|
2021-04-01 09:27:39 +00:00
|
|
|
#include "ui/text/format_values.h"
|
2021-03-29 12:16:54 +00:00
|
|
|
#include "ui/ui_utility.h"
|
|
|
|
#include "ui/special_fields.h"
|
2021-08-26 14:46:24 +00:00
|
|
|
#include "countries/countries_instance.h"
|
2021-03-26 17:09:09 +00:00
|
|
|
#include "base/platform/base_platform_info.h"
|
2021-03-29 12:16:54 +00:00
|
|
|
#include "base/event_filter.h"
|
2021-10-19 13:00:21 +00:00
|
|
|
#include "base/qt_adapters.h"
|
2021-03-26 15:23:12 +00:00
|
|
|
#include "styles/style_payments.h"
|
|
|
|
|
2021-04-01 09:27:39 +00:00
|
|
|
#include <QtCore/QRegularExpression>
|
|
|
|
|
2021-03-26 15:23:12 +00:00
|
|
|
namespace Payments::Ui {
|
|
|
|
namespace {
|
|
|
|
|
2021-04-01 09:27:39 +00:00
|
|
|
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) {
|
|
|
|
return value.replace(QRegularExpression("[^0-9]"), 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);
|
2021-10-19 13:00:21 +00:00
|
|
|
auto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));
|
2021-04-01 09:27:39 +00:00
|
|
|
if (decimalPosition < 0) {
|
|
|
|
state = {
|
|
|
|
.value = RemoveNonNumbers(state.value),
|
2021-10-19 13:00:21 +00:00
|
|
|
.position = int(RemoveNonNumbers(beforePosition).size()),
|
2021-04-01 09:27:39 +00:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
|
|
|
|
state.value.mid(0, decimalPosition));
|
|
|
|
state = {
|
|
|
|
.value = (onlyNumbersBeforeDecimal
|
|
|
|
+ QChar(rule.decimal)
|
|
|
|
+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
|
2021-10-19 13:00:21 +00:00
|
|
|
.position = int(RemoveNonNumbers(beforePosition).size()
|
2021-04-01 09:27:39 +00:00
|
|
|
+ (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;
|
2021-04-19 06:52:14 +00:00
|
|
|
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) {
|
2021-04-01 09:27:39 +00:00
|
|
|
state = {
|
2021-04-19 06:52:14 +00:00
|
|
|
.value = state.value.mid(0, digitsLimit),
|
|
|
|
.position = std::min(state.position, digitsLimit),
|
2021-04-01 09:27:39 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
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)
|
2021-10-19 13:00:21 +00:00
|
|
|
&& (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));
|
2021-04-01 09:27:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
|
|
|
|
return (request.wasAnchor == request.wasPosition)
|
|
|
|
&& (request.wasPosition == request.nowPosition)
|
2021-10-19 13:00:21 +00:00
|
|
|
&& (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));
|
2021-04-01 09:27:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
[[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,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-03-26 17:09:09 +00:00
|
|
|
[[nodiscard]] QString Parse(const FieldConfig &config) {
|
|
|
|
if (config.type == FieldType::Country) {
|
2021-08-26 15:15:49 +00:00
|
|
|
return Countries::Instance().countryNameByISO2(config.value);
|
2021-04-01 09:27:39 +00:00
|
|
|
} 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));
|
2021-03-26 17:09:09 +00:00
|
|
|
}
|
|
|
|
return config.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] QString Format(
|
|
|
|
const FieldConfig &config,
|
|
|
|
const QString &parsed,
|
|
|
|
const QString &countryIso2) {
|
|
|
|
if (config.type == FieldType::Country) {
|
|
|
|
return countryIso2;
|
2021-04-01 09:27:39 +00:00
|
|
|
} else if (config.type == FieldType::Money) {
|
|
|
|
const auto rule = LookupCurrencyRule(config.currency);
|
|
|
|
const auto real = QString(parsed).replace(
|
|
|
|
QChar(rule.decimal),
|
|
|
|
QChar('.')
|
|
|
|
).replace(
|
|
|
|
QChar(','),
|
|
|
|
QChar('.')
|
|
|
|
).replace(
|
|
|
|
QRegularExpression("[^0-9\\.]"),
|
|
|
|
QString()
|
|
|
|
).toDouble();
|
|
|
|
return QString::number(
|
2021-09-27 08:13:57 +00:00
|
|
|
int64(base::SafeRound(real * std::pow(10., rule.exponent))));
|
2021-04-07 12:50:55 +00:00
|
|
|
} else if (config.type == FieldType::CardNumber
|
|
|
|
|| config.type == FieldType::CardCVC) {
|
|
|
|
return QString(parsed).replace(
|
|
|
|
QRegularExpression("[^0-9\\.]"),
|
|
|
|
QString());
|
2021-03-26 17:09:09 +00:00
|
|
|
}
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
|
2021-03-26 15:23:12 +00:00
|
|
|
[[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:
|
2021-04-01 09:27:39 +00:00
|
|
|
case FieldType::Money:
|
2021-03-26 15:23:12 +00:00
|
|
|
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),
|
2021-03-26 17:09:09 +00:00
|
|
|
Parse(config));
|
2021-03-26 15:23:12 +00:00
|
|
|
case FieldType::CardNumber:
|
|
|
|
case FieldType::CardExpireDate:
|
|
|
|
case FieldType::CardCVC:
|
|
|
|
case FieldType::Country:
|
|
|
|
case FieldType::Phone:
|
2021-04-01 09:27:39 +00:00
|
|
|
case FieldType::Money:
|
2021-03-26 15:23:12 +00:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2021-04-01 09:27:39 +00:00
|
|
|
[[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),
|
2021-04-02 12:59:54 +00:00
|
|
|
.st = st::paymentsMoneyField,
|
2021-04-01 09:27:39 +00:00
|
|
|
});
|
|
|
|
const auto &rule = state->rule;
|
|
|
|
state->currencySkip = rule.space ? state->st.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->naturalWidth() + 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.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)
|
2021-10-19 13:00:21 +00:00
|
|
|
? std::max(rule.exponent - int(text.size() - decimal - 1), 0)
|
2021-04-01 09:27:39 +00:00
|
|
|
: 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;
|
|
|
|
}
|
|
|
|
|
2021-03-26 15:23:12 +00:00
|
|
|
[[nodiscard]] MaskedInputField *LookupMaskedField(
|
|
|
|
not_null<RpWidget*> wrap,
|
2021-04-01 09:27:39 +00:00
|
|
|
FieldConfig &config,
|
|
|
|
rpl::producer<> textPossiblyChanged) {
|
2021-03-26 15:23:12 +00:00
|
|
|
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),
|
2021-03-26 17:09:09 +00:00
|
|
|
Parse(config));
|
2021-03-29 12:16:54 +00:00
|
|
|
case FieldType::Phone:
|
|
|
|
return CreateChild<PhoneInput>(
|
|
|
|
wrap.get(),
|
|
|
|
st::paymentsField,
|
|
|
|
std::move(config.placeholder),
|
2021-08-29 19:15:35 +00:00
|
|
|
Countries::ExtractPhoneCode(config.defaultPhone),
|
2021-03-29 12:16:54 +00:00
|
|
|
Parse(config));
|
2021-04-01 09:27:39 +00:00
|
|
|
case FieldType::Money:
|
|
|
|
return CreateMoneyField(
|
|
|
|
wrap,
|
|
|
|
config,
|
|
|
|
std::move(textPossiblyChanged));
|
2021-03-26 15:23:12 +00:00
|
|
|
}
|
|
|
|
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
Field::Field(QWidget *parent, FieldConfig &&config)
|
2021-03-26 17:09:09 +00:00
|
|
|
: _config(config)
|
2021-03-26 15:23:12 +00:00
|
|
|
, _wrap(CreateWrap(parent, config))
|
|
|
|
, _input(LookupInputField(_wrap.get(), config))
|
2021-04-01 09:27:39 +00:00
|
|
|
, _masked(LookupMaskedField(
|
|
|
|
_wrap.get(),
|
|
|
|
config,
|
|
|
|
_textPossiblyChanged.events_starting_with({})))
|
2021-03-26 17:09:09 +00:00
|
|
|
, _countryIso2(config.value) {
|
2021-03-26 15:23:12 +00:00
|
|
|
if (_masked) {
|
2021-03-26 17:09:09 +00:00
|
|
|
setupMaskedGeometry();
|
|
|
|
}
|
|
|
|
if (_config.type == FieldType::Country) {
|
|
|
|
setupCountry();
|
2021-03-26 15:23:12 +00:00
|
|
|
}
|
2021-03-29 12:16:54 +00:00
|
|
|
if (const auto &validator = config.validator) {
|
|
|
|
setupValidator(validator);
|
2021-04-01 09:27:39 +00:00
|
|
|
} else if (config.type == FieldType::Money) {
|
|
|
|
setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
|
2021-03-29 12:16:54 +00:00
|
|
|
}
|
|
|
|
setupFrontBackspace();
|
2021-04-01 15:06:48 +00:00
|
|
|
setupSubmit();
|
2021-03-26 15:23:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RpWidget *Field::widget() const {
|
|
|
|
return _wrap.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
object_ptr<RpWidget> Field::ownedWidget() const {
|
|
|
|
return object_ptr<RpWidget>::fromRaw(_wrap.get());
|
|
|
|
}
|
|
|
|
|
2021-03-26 17:09:09 +00:00
|
|
|
QString Field::value() const {
|
|
|
|
return Format(
|
|
|
|
_config,
|
|
|
|
_input ? _input->getLastText() : _masked->getLastText(),
|
|
|
|
_countryIso2);
|
|
|
|
}
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
rpl::producer<> Field::frontBackspace() const {
|
|
|
|
return _frontBackspace.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Field::finished() const {
|
|
|
|
return _finished.events();
|
|
|
|
}
|
|
|
|
|
2021-04-01 15:06:48 +00:00
|
|
|
rpl::producer<> Field::submitted() const {
|
|
|
|
return _submitted.events();
|
|
|
|
}
|
|
|
|
|
2021-03-26 17:09:09 +00:00
|
|
|
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();
|
|
|
|
|
2021-08-26 15:15:49 +00:00
|
|
|
const auto name = Countries::Instance().countryNameByISO2(
|
|
|
|
_countryIso2);
|
2021-03-26 17:09:09 +00:00
|
|
|
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;
|
2021-08-26 15:15:49 +00:00
|
|
|
_masked->setText(Countries::Instance().countryNameByISO2(iso2));
|
2021-03-26 17:09:09 +00:00
|
|
|
_masked->hideError();
|
|
|
|
raw->closeBox();
|
2021-04-01 15:06:48 +00:00
|
|
|
if (!iso2.isEmpty()) {
|
|
|
|
if (_nextField) {
|
|
|
|
_nextField->activate();
|
|
|
|
} else {
|
|
|
|
_submitted.fire({});
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 17:09:09 +00:00
|
|
|
}, _masked->lifetime());
|
2021-03-29 12:16:54 +00:00
|
|
|
raw->boxClosing() | rpl::start_with_next([=] {
|
|
|
|
setFocus();
|
|
|
|
}, _masked->lifetime());
|
2021-03-26 17:09:09 +00:00
|
|
|
_config.showBox(std::move(box));
|
|
|
|
});
|
2021-03-26 15:23:12 +00:00
|
|
|
}
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
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 {
|
2021-04-01 09:27:39 +00:00
|
|
|
.value = _masked->getLastText(),
|
2021-03-29 12:16:54 +00:00
|
|
|
.position = position,
|
|
|
|
.anchor = (selectionStart == selectionEnd
|
|
|
|
? position
|
|
|
|
: (selectionStart == position)
|
|
|
|
? selectionEnd
|
|
|
|
: selectionStart),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
const auto cursor = _input->textCursor();
|
|
|
|
return {
|
2021-04-01 09:27:39 +00:00
|
|
|
.value = _input->getLastText(),
|
2021-03-29 12:16:54 +00:00
|
|
|
.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();
|
2021-04-01 09:27:39 +00:00
|
|
|
_textPossiblyChanged.fire({});
|
2021-03-29 12:16:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const auto now = state();
|
|
|
|
const auto result = validator(ValidateRequest{
|
|
|
|
.wasValue = _was.value,
|
|
|
|
.wasPosition = _was.position,
|
|
|
|
.wasAnchor = _was.anchor,
|
|
|
|
.nowValue = now.value,
|
|
|
|
.nowPosition = now.position,
|
|
|
|
});
|
2021-04-01 15:06:48 +00:00
|
|
|
_valid = result.finished || !result.invalid;
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
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);
|
|
|
|
QObject::connect(_input, &InputField::changed, validate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-01 15:06:48 +00:00
|
|
|
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 {
|
|
|
|
QObject::connect(_input, &InputField::submitted, submitted);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
void Field::setNextField(not_null<Field*> field) {
|
2021-04-01 15:06:48 +00:00
|
|
|
_nextField = field;
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2021-04-01 15:06:48 +00:00
|
|
|
void Field::activate() {
|
|
|
|
if (_input) {
|
|
|
|
_input->setFocus();
|
|
|
|
} else {
|
|
|
|
_masked->setFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-26 15:23:12 +00:00
|
|
|
void Field::setFocus() {
|
2021-03-26 17:09:09 +00:00
|
|
|
if (_config.type == FieldType::Country) {
|
|
|
|
_wrap->setFocus();
|
2021-03-26 15:23:12 +00:00
|
|
|
} else {
|
2021-04-01 15:06:48 +00:00
|
|
|
activate();
|
2021-03-26 15:23:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Field::setFocusFast() {
|
2021-03-26 17:09:09 +00:00
|
|
|
if (_config.type == FieldType::Country) {
|
|
|
|
setFocus();
|
|
|
|
} else if (_input) {
|
2021-03-26 15:23:12 +00:00
|
|
|
_input->setFocusFast();
|
|
|
|
} else {
|
|
|
|
_masked->setFocusFast();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Field::showError() {
|
2021-03-26 17:09:09 +00:00
|
|
|
if (_config.type == FieldType::Country) {
|
|
|
|
setFocus();
|
|
|
|
_masked->showErrorNoFocus();
|
|
|
|
} else if (_input) {
|
2021-03-26 15:23:12 +00:00
|
|
|
_input->showError();
|
|
|
|
} else {
|
|
|
|
_masked->showError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-29 12:16:54 +00:00
|
|
|
void Field::showErrorNoFocus() {
|
|
|
|
if (_input) {
|
|
|
|
_input->showErrorNoFocus();
|
|
|
|
} else {
|
|
|
|
_masked->showErrorNoFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-26 15:23:12 +00:00
|
|
|
} // namespace Payments::Ui
|