715 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			715 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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"
 | |
| #include "ui/boxes/country_select_box.h"
 | |
| #include "ui/text/format_values.h"
 | |
| #include "ui/ui_utility.h"
 | |
| #include "ui/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>
 | |
| 
 | |
| 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) {
 | |
| 	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);
 | |
| 	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) {
 | |
| 		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(
 | |
| 			int64(base::SafeRound(real * std::pow(10., rule.exponent))));
 | |
| 	} else if (config.type == FieldType::CardNumber
 | |
| 		|| config.type == FieldType::CardCVC) {
 | |
| 		return QString(parsed).replace(
 | |
| 			QRegularExpression("[^0-9\\.]"),
 | |
| 			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.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)
 | |
| 			? 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);
 | |
| 		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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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
 | 
