499 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
	
		
			12 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 "passport/passport_panel_edit_document.h"
 | |
| 
 | |
| #include "passport/passport_panel_controller.h"
 | |
| #include "passport/passport_panel_details_row.h"
 | |
| #include "passport/passport_panel_edit_scans.h"
 | |
| #include "info/profile/info_profile_button.h"
 | |
| #include "info/profile/info_profile_values.h"
 | |
| #include "ui/widgets/input_fields.h"
 | |
| #include "ui/widgets/scroll_area.h"
 | |
| #include "ui/widgets/labels.h"
 | |
| #include "ui/widgets/buttons.h"
 | |
| #include "ui/widgets/shadow.h"
 | |
| #include "ui/widgets/checkbox.h"
 | |
| #include "ui/wrap/vertical_layout.h"
 | |
| #include "ui/wrap/fade_wrap.h"
 | |
| #include "boxes/abstract_box.h"
 | |
| #include "boxes/confirm_box.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "styles/style_widgets.h"
 | |
| #include "styles/style_boxes.h"
 | |
| #include "styles/style_passport.h"
 | |
| 
 | |
| namespace Passport {
 | |
| namespace {
 | |
| 
 | |
| class RequestTypeBox : public BoxContent {
 | |
| public:
 | |
| 	RequestTypeBox(
 | |
| 		QWidget*,
 | |
| 		const QString &title,
 | |
| 		const QString &about,
 | |
| 		std::vector<QString> labels,
 | |
| 		Fn<void(int index)> submit);
 | |
| 
 | |
| protected:
 | |
| 	void prepare() override;
 | |
| 
 | |
| private:
 | |
| 	void setupControls(
 | |
| 		const QString &about,
 | |
| 		std::vector<QString> labels,
 | |
| 		Fn<void(int index)> submit);
 | |
| 
 | |
| 	QString _title;
 | |
| 	Fn<void()> _submit;
 | |
| 	int _height = 0;
 | |
| 
 | |
| };
 | |
| 
 | |
| class DeleteDocumentBox : public BoxContent {
 | |
| public:
 | |
| 	DeleteDocumentBox(
 | |
| 		QWidget*,
 | |
| 		const QString &text,
 | |
| 		const QString &detailsCheckbox,
 | |
| 		Fn<void(bool withDetails)> submit);
 | |
| 
 | |
| protected:
 | |
| 	void prepare() override;
 | |
| 
 | |
| private:
 | |
| 	void setupControls(
 | |
| 		const QString &text,
 | |
| 		const QString &detailsCheckbox,
 | |
| 		Fn<void(bool withDetails)> submit);
 | |
| 
 | |
| 	Fn<void()> _submit;
 | |
| 	int _height = 0;
 | |
| 
 | |
| };
 | |
| 
 | |
| RequestTypeBox::RequestTypeBox(
 | |
| 	QWidget*,
 | |
| 	const QString &title,
 | |
| 	const QString &about,
 | |
| 	std::vector<QString> labels,
 | |
| 	Fn<void(int index)> submit)
 | |
| : _title(title) {
 | |
| 	setupControls(about, std::move(labels), submit);
 | |
| }
 | |
| 
 | |
| void RequestTypeBox::prepare() {
 | |
| 	setTitle([=] { return _title; });
 | |
| 	addButton(langFactory(lng_passport_upload_document), _submit);
 | |
| 	addButton(langFactory(lng_cancel), [=] { closeBox(); });
 | |
| 	setDimensions(st::boxWidth, _height);
 | |
| }
 | |
| 
 | |
| void RequestTypeBox::setupControls(
 | |
| 		const QString &about,
 | |
| 		std::vector<QString> labels,
 | |
| 		Fn<void(int index)> submit) {
 | |
| 	const auto header = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		this,
 | |
| 		lang(lng_passport_document_type),
 | |
| 		Ui::FlatLabel::InitType::Simple,
 | |
| 		st::boxDividerLabel);
 | |
| 
 | |
| 	const auto group = std::make_shared<Ui::RadiobuttonGroup>(0);
 | |
| 	auto buttons = std::vector<QPointer<Ui::Radiobutton>>();
 | |
| 	auto index = 0;
 | |
| 	for (const auto &label : labels) {
 | |
| 		buttons.push_back(Ui::CreateChild<Ui::Radiobutton>(
 | |
| 			this,
 | |
| 			group,
 | |
| 			index++,
 | |
| 			label,
 | |
| 			st::defaultBoxCheckbox));
 | |
| 	}
 | |
| 
 | |
| 	const auto description = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		this,
 | |
| 		about,
 | |
| 		Ui::FlatLabel::InitType::Simple,
 | |
| 		st::boxDividerLabel);
 | |
| 
 | |
| 	auto y = 0;
 | |
| 	const auto innerWidth = st::boxWidth
 | |
| 		- st::boxPadding.left()
 | |
| 		- st::boxPadding.right();
 | |
| 	header->resizeToWidth(innerWidth);
 | |
| 	header->moveToLeft(st::boxPadding.left(), y);
 | |
| 	y += header->height() + st::passportRequestTypeSkip;
 | |
| 	for (const auto &button : buttons) {
 | |
| 		button->resizeToNaturalWidth(innerWidth);
 | |
| 		button->moveToLeft(st::boxPadding.left(), y);
 | |
| 		y += button->heightNoMargins() + st::passportRequestTypeSkip;
 | |
| 	}
 | |
| 	description->resizeToWidth(innerWidth);
 | |
| 	description->moveToLeft(st::boxPadding.left(), y);
 | |
| 	y += description->height() + st::passportRequestTypeSkip;
 | |
| 	_height = y;
 | |
| 
 | |
| 	_submit = [=] {
 | |
| 		const auto value = group->hasValue() ? group->value() : -1;
 | |
| 		if (value >= 0) {
 | |
| 			submit(value);
 | |
| 		}
 | |
| 	};
 | |
| }
 | |
| 
 | |
| DeleteDocumentBox::DeleteDocumentBox(
 | |
| 		QWidget*,
 | |
| 		const QString &text,
 | |
| 		const QString &detailsCheckbox,
 | |
| 		Fn<void(bool withDetails)> submit) {
 | |
| 	setupControls(text, detailsCheckbox, submit);
 | |
| }
 | |
| 
 | |
| void DeleteDocumentBox::prepare() {
 | |
| 	addButton(langFactory(lng_box_delete), _submit);
 | |
| 	addButton(langFactory(lng_cancel), [=] { closeBox(); });
 | |
| 
 | |
| 	setDimensions(st::boxWidth, _height);
 | |
| }
 | |
| 
 | |
| void DeleteDocumentBox::setupControls(
 | |
| 		const QString &text,
 | |
| 		const QString &detailsCheckbox,
 | |
| 		Fn<void(bool withDetails)> submit) {
 | |
| 	const auto label = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		this,
 | |
| 		text,
 | |
| 		Ui::FlatLabel::InitType::Simple,
 | |
| 		st::boxLabel);
 | |
| 	const auto details = !detailsCheckbox.isEmpty()
 | |
| 		? Ui::CreateChild<Ui::Checkbox>(
 | |
| 			this,
 | |
| 			detailsCheckbox,
 | |
| 			false,
 | |
| 			st::defaultBoxCheckbox)
 | |
| 		: nullptr;
 | |
| 
 | |
| 	_height = st::boxPadding.top();
 | |
| 	const auto availableWidth = st::boxWidth
 | |
| 		- st::boxPadding.left()
 | |
| 		- st::boxPadding.right();
 | |
| 	label->resizeToWidth(availableWidth);
 | |
| 	label->moveToLeft(st::boxPadding.left(), _height);
 | |
| 	_height += label->height();
 | |
| 
 | |
| 	if (details) {
 | |
| 		_height += st::boxPadding.bottom();
 | |
| 		details->moveToLeft(st::boxPadding.left(), _height);
 | |
| 		_height += details->heightNoMargins();
 | |
| 	}
 | |
| 	_height += st::boxPadding.bottom();
 | |
| 
 | |
| 	_submit = [=] {
 | |
| 		submit(details ? details->checked() : false);
 | |
| 	};
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct PanelEditDocument::Result {
 | |
| 	ValueMap data;
 | |
| 	ValueMap filesData;
 | |
| };
 | |
| 
 | |
| PanelEditDocument::PanelEditDocument(
 | |
| 	QWidget*,
 | |
| 	not_null<PanelController*> controller,
 | |
| 	Scheme scheme,
 | |
| 	const ValueMap &data,
 | |
| 	const ValueMap &scanData,
 | |
| 	const QString &missingScansError,
 | |
| 	std::vector<ScanInfo> &&files,
 | |
| 	std::map<SpecialFile, ScanInfo> &&specialFiles)
 | |
| : _controller(controller)
 | |
| , _scheme(std::move(scheme))
 | |
| , _scroll(this, st::passportPanelScroll)
 | |
| , _topShadow(this)
 | |
| , _bottomShadow(this)
 | |
| , _done(
 | |
| 		this,
 | |
| 		langFactory(lng_passport_save_value),
 | |
| 		st::passportPanelSaveValue) {
 | |
| 	setupControls(
 | |
| 		data,
 | |
| 		&scanData,
 | |
| 		missingScansError,
 | |
| 		std::move(files),
 | |
| 		std::move(specialFiles));
 | |
| }
 | |
| 
 | |
| PanelEditDocument::PanelEditDocument(
 | |
| 	QWidget*,
 | |
| 	not_null<PanelController*> controller,
 | |
| 	Scheme scheme,
 | |
| 	const ValueMap &data)
 | |
| : _controller(controller)
 | |
| , _scheme(std::move(scheme))
 | |
| , _scroll(this, st::passportPanelScroll)
 | |
| , _topShadow(this)
 | |
| , _bottomShadow(this)
 | |
| , _done(
 | |
| 		this,
 | |
| 		langFactory(lng_passport_save_value),
 | |
| 		st::passportPanelSaveValue) {
 | |
| 	setupControls(data, nullptr, QString(), {}, {});
 | |
| }
 | |
| 
 | |
| void PanelEditDocument::setupControls(
 | |
| 		const ValueMap &data,
 | |
| 		const ValueMap *scanData,
 | |
| 		const QString &missingScansError,
 | |
| 		std::vector<ScanInfo> &&files,
 | |
| 		std::map<SpecialFile, ScanInfo> &&specialFiles) {
 | |
| 	const auto inner = setupContent(
 | |
| 		data,
 | |
| 		scanData,
 | |
| 		missingScansError,
 | |
| 		std::move(files),
 | |
| 		std::move(specialFiles));
 | |
| 
 | |
| 	using namespace rpl::mappers;
 | |
| 
 | |
| 	_topShadow->toggleOn(
 | |
| 		_scroll->scrollTopValue() | rpl::map(_1 > 0));
 | |
| 	_done->addClickHandler([=] {
 | |
| 		crl::on_main(this, [=] {
 | |
| 			save();
 | |
| 		});
 | |
| 	});
 | |
| }
 | |
| 
 | |
| not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
 | |
| 		const ValueMap &data,
 | |
| 		const ValueMap *scanData,
 | |
| 		const QString &missingScansError,
 | |
| 		std::vector<ScanInfo> &&files,
 | |
| 		std::map<SpecialFile, ScanInfo> &&specialFiles) {
 | |
| 	const auto inner = _scroll->setOwnedWidget(
 | |
| 		object_ptr<Ui::VerticalLayout>(this));
 | |
| 	_scroll->widthValue(
 | |
| 	) | rpl::start_with_next([=](int width) {
 | |
| 		inner->resizeToWidth(width);
 | |
| 	}, inner->lifetime());
 | |
| 
 | |
| 	if (!specialFiles.empty()) {
 | |
| 		_editScans = inner->add(
 | |
| 			object_ptr<EditScans>(
 | |
| 				inner,
 | |
| 				_controller,
 | |
| //				_scheme.scansHeader,
 | |
| //				missingScansError,
 | |
| //				std::move(files),
 | |
| 				std::move(specialFiles)));
 | |
| 	} else if (scanData) {
 | |
| 		_editScans = inner->add(
 | |
| 			object_ptr<EditScans>(
 | |
| 				inner,
 | |
| 				_controller,
 | |
| 				_scheme.scansHeader,
 | |
| 				missingScansError,
 | |
| 				std::move(files)));
 | |
| 	} else {
 | |
| 		inner->add(object_ptr<BoxContentDivider>(
 | |
| 			inner,
 | |
| 			st::passportFormDividerHeight));
 | |
| 	}
 | |
| 
 | |
| 	inner->add(
 | |
| 		object_ptr<Ui::FlatLabel>(
 | |
| 			inner,
 | |
| 			_scheme.rowsHeader,
 | |
| 			Ui::FlatLabel::InitType::Simple,
 | |
| 			st::passportFormHeader),
 | |
| 		st::passportDetailsHeaderPadding);
 | |
| 
 | |
| 	const auto valueOrEmpty = [&](
 | |
| 			const ValueMap &values,
 | |
| 			const QString &key) {
 | |
| 		const auto &fields = values.fields;
 | |
| 		if (const auto i = fields.find(key); i != fields.end()) {
 | |
| 			return i->second;
 | |
| 		}
 | |
| 		return ValueField();
 | |
| 	};
 | |
| 
 | |
| 	const auto enumerateRows = [&](auto &&callback) {
 | |
| 		for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
 | |
| 			const auto &row = _scheme.rows[i];
 | |
| 			auto fields = (row.valueClass == Scheme::ValueClass::Fields)
 | |
| 				? &data
 | |
| 				: scanData;
 | |
| 			if (!fields) {
 | |
| 				continue;
 | |
| 			}
 | |
| 			callback(i, row, *fields);
 | |
| 		}
 | |
| 	};
 | |
| 	auto maxLabelWidth = 0;
 | |
| 	enumerateRows([&](
 | |
| 			int i,
 | |
| 			const EditDocumentScheme::Row &row,
 | |
| 			const ValueMap &fields) {
 | |
| 		accumulate_max(
 | |
| 			maxLabelWidth,
 | |
| 			PanelDetailsRow::LabelWidth(row.label));
 | |
| 	});
 | |
| 	enumerateRows([&](
 | |
| 			int i,
 | |
| 			const EditDocumentScheme::Row &row,
 | |
| 			const ValueMap &fields) {
 | |
| 		const auto current = valueOrEmpty(fields, row.key);
 | |
| 		_details.emplace(i, inner->add(PanelDetailsRow::Create(
 | |
| 			inner,
 | |
| 			row.inputType,
 | |
| 			_controller,
 | |
| 			row.label,
 | |
| 			maxLabelWidth,
 | |
| 			current.text,
 | |
| 			current.error,
 | |
| 			row.lengthLimit)));
 | |
| 	});
 | |
| 
 | |
| 	inner->add(
 | |
| 		object_ptr<Ui::FixedHeightWidget>(inner, st::passportDetailsSkip));
 | |
| 	if (auto text = _controller->deleteValueLabel()) {
 | |
| 		inner->add(
 | |
| 			object_ptr<Info::Profile::Button>(
 | |
| 				inner,
 | |
| 				std::move(*text) | Info::Profile::ToUpperValue(),
 | |
| 				st::passportDeleteButton),
 | |
| 			st::passportUploadButtonPadding
 | |
| 		)->addClickHandler([=] {
 | |
| 			_controller->deleteValue();
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	return inner;
 | |
| }
 | |
| 
 | |
| void PanelEditDocument::focusInEvent(QFocusEvent *e) {
 | |
| 	crl::on_main(this, [=] {
 | |
| 		for (const auto [index, row] : _details) {
 | |
| 			if (row->setFocusFast()) {
 | |
| 				return;
 | |
| 			}
 | |
| 		}
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void PanelEditDocument::resizeEvent(QResizeEvent *e) {
 | |
| 	updateControlsGeometry();
 | |
| }
 | |
| 
 | |
| bool PanelEditDocument::hasUnsavedChanges() const {
 | |
| 	const auto result = collect();
 | |
| 	return _controller->editScopeChanged(result.data, result.filesData);
 | |
| }
 | |
| 
 | |
| void PanelEditDocument::updateControlsGeometry() {
 | |
| 	const auto submitTop = height() - _done->height();
 | |
| 	_scroll->setGeometry(0, 0, width(), submitTop);
 | |
| 	_topShadow->resizeToWidth(width());
 | |
| 	_topShadow->moveToLeft(0, 0);
 | |
| 	_bottomShadow->resizeToWidth(width());
 | |
| 	_bottomShadow->moveToLeft(0, submitTop - st::lineWidth);
 | |
| 	_done->resizeToWidth(width());
 | |
| 	_done->moveToLeft(0, submitTop);
 | |
| 
 | |
| 	_scroll->updateBars();
 | |
| }
 | |
| 
 | |
| PanelEditDocument::Result PanelEditDocument::collect() const {
 | |
| 	auto result = Result();
 | |
| 	for (const auto [i, field] : _details) {
 | |
| 		const auto &row = _scheme.rows[i];
 | |
| 		auto &fields = (row.valueClass == Scheme::ValueClass::Fields)
 | |
| 			? result.data
 | |
| 			: result.filesData;
 | |
| 		fields.fields[row.key].text = field->valueCurrent();
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| bool PanelEditDocument::validate() {
 | |
| 	const auto error = _editScans
 | |
| 		? _editScans->validateGetErrorTop()
 | |
| 		: base::none;
 | |
| 	if (error) {
 | |
| 		const auto errortop = _editScans->mapToGlobal(QPoint(0, *error));
 | |
| 		const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
 | |
| 		const auto scrolldelta = errortop.y() - scrolltop.y();
 | |
| 		_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
 | |
| 	}
 | |
| 	auto first = QPointer<PanelDetailsRow>();
 | |
| 	for (const auto [i, field] : base::reversed(_details)) {
 | |
| 		const auto &row = _scheme.rows[i];
 | |
| 		if (field->errorShown()) {
 | |
| 			field->showError();
 | |
| 			first = field;
 | |
| 		} else if (row.error) {
 | |
| 			if (const auto error = row.error(field->valueCurrent())) {
 | |
| 				field->showError(error);
 | |
| 				first = field;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if (error) {
 | |
| 		return false;
 | |
| 	} else if (!first) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	const auto firsttop = first->mapToGlobal(QPoint(0, 0));
 | |
| 	const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
 | |
| 	const auto scrolldelta = firsttop.y() - scrolltop.y();
 | |
| 	_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void PanelEditDocument::save() {
 | |
| 	if (!validate()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	auto result = collect();
 | |
| 	_controller->saveScope(
 | |
| 		std::move(result.data),
 | |
| 		std::move(result.filesData));
 | |
| }
 | |
| 
 | |
| object_ptr<BoxContent> RequestIdentityType(
 | |
| 		Fn<void(int index)> submit,
 | |
| 		std::vector<QString> labels) {
 | |
| 	return Box<RequestTypeBox>(
 | |
| 		lang(lng_passport_identity_title),
 | |
| 		lang(lng_passport_identity_about),
 | |
| 		std::move(labels),
 | |
| 		submit);
 | |
| }
 | |
| 
 | |
| object_ptr<BoxContent> RequestAddressType(
 | |
| 		Fn<void(int index)> submit,
 | |
| 		std::vector<QString> labels) {
 | |
| 	return Box<RequestTypeBox>(
 | |
| 		lang(lng_passport_address_title),
 | |
| 		lang(lng_passport_address_about),
 | |
| 		std::move(labels),
 | |
| 		submit);
 | |
| }
 | |
| 
 | |
| object_ptr<BoxContent> ConfirmDeleteDocument(
 | |
| 		Fn<void(bool withDetails)> submit,
 | |
| 		const QString &text,
 | |
| 		const QString &detailsCheckbox) {
 | |
| 	return Box<DeleteDocumentBox>(text, detailsCheckbox, submit);
 | |
| }
 | |
| 
 | |
| } // namespace Passport
 | 
