732 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			732 lines
		
	
	
	
		
			18 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 "boxes/create_poll_box.h"
 | 
						|
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "data/data_poll.h"
 | 
						|
#include "ui/toast/toast.h"
 | 
						|
#include "ui/wrap/vertical_layout.h"
 | 
						|
#include "ui/wrap/slide_wrap.h"
 | 
						|
#include "ui/widgets/input_fields.h"
 | 
						|
#include "ui/widgets/shadow.h"
 | 
						|
#include "ui/widgets/labels.h"
 | 
						|
#include "ui/widgets/buttons.h"
 | 
						|
#include "core/event_filter.h"
 | 
						|
#include "chat_helpers/emoji_suggestions_widget.h"
 | 
						|
#include "settings/settings_common.h"
 | 
						|
#include "base/unique_qptr.h"
 | 
						|
#include "styles/style_boxes.h"
 | 
						|
#include "styles/style_settings.h"
 | 
						|
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kQuestionLimit = 255;
 | 
						|
constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
 | 
						|
constexpr auto kOptionLimit = 100;
 | 
						|
constexpr auto kWarnQuestionLimit = 80;
 | 
						|
constexpr auto kWarnOptionLimit = 30;
 | 
						|
constexpr auto kErrorLimit = 99;
 | 
						|
 | 
						|
class Options {
 | 
						|
public:
 | 
						|
	Options(
 | 
						|
		not_null<QWidget*> outer,
 | 
						|
		not_null<Ui::VerticalLayout*> container);
 | 
						|
 | 
						|
	[[nodiscard]] bool isValid() const;
 | 
						|
	[[nodiscard]] rpl::producer<bool> isValidChanged() const;
 | 
						|
	[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
 | 
						|
	void focusFirst();
 | 
						|
 | 
						|
	[[nodiscard]] rpl::producer<int> usedCount() const;
 | 
						|
	[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
 | 
						|
	[[nodiscard]] rpl::producer<> backspaceInFront() const;
 | 
						|
 | 
						|
private:
 | 
						|
	class Option {
 | 
						|
	public:
 | 
						|
		static Option Create(
 | 
						|
			not_null<QWidget*> outer,
 | 
						|
			not_null<Ui::VerticalLayout*> container,
 | 
						|
			int position);
 | 
						|
 | 
						|
		void toggleRemoveAlways(bool toggled);
 | 
						|
 | 
						|
		void show(anim::type animated);
 | 
						|
		void destroy(FnMut<void()> done);
 | 
						|
 | 
						|
		//[[nodisacrd]] bool hasShadow() const;
 | 
						|
		//void destroyShadow();
 | 
						|
 | 
						|
		[[nodiscard]] bool isEmpty() const;
 | 
						|
		[[nodiscard]] bool isGood() const;
 | 
						|
		[[nodiscard]] bool isTooLong() const;
 | 
						|
		[[nodiscard]] bool hasFocus() const;
 | 
						|
		void setFocus() const;
 | 
						|
		void clearValue();
 | 
						|
 | 
						|
		void setPlaceholder() const;
 | 
						|
		void removePlaceholder() const;
 | 
						|
 | 
						|
		not_null<Ui::InputField*> field() const;
 | 
						|
 | 
						|
		[[nodiscard]] PollAnswer toPollAnswer(int index) const;
 | 
						|
 | 
						|
		[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
 | 
						|
 | 
						|
		inline bool operator<(const Option &other) const {
 | 
						|
			return field() < other.field();
 | 
						|
		}
 | 
						|
 | 
						|
		friend inline bool operator<(
 | 
						|
				const Option &option,
 | 
						|
				Ui::InputField *field) {
 | 
						|
			return option.field() < field;
 | 
						|
		}
 | 
						|
		friend inline bool operator<(
 | 
						|
				Ui::InputField *field,
 | 
						|
				const Option &option) {
 | 
						|
			return field < option.field();
 | 
						|
		}
 | 
						|
 | 
						|
	private:
 | 
						|
		Option() = default;
 | 
						|
 | 
						|
		void createShadow();
 | 
						|
		void createRemove();
 | 
						|
		void createWarning();
 | 
						|
 | 
						|
		base::unique_qptr<Ui::SlideWrap<Ui::InputField>> _field;
 | 
						|
		base::unique_qptr<Ui::PlainShadow> _shadow;
 | 
						|
		base::unique_qptr<Ui::CrossButton> _remove;
 | 
						|
		rpl::variable<bool> *_removeAlways = nullptr;
 | 
						|
 | 
						|
	};
 | 
						|
 | 
						|
	[[nodiscard]] bool full() const;
 | 
						|
	//[[nodiscard]] bool correctShadows() const;
 | 
						|
	//void fixShadows();
 | 
						|
	void removeEmptyTail();
 | 
						|
	void addEmptyOption();
 | 
						|
	void checkLastOption();
 | 
						|
	void validateState();
 | 
						|
	void fixAfterErase();
 | 
						|
	void destroy(Option &&option);
 | 
						|
	void removeDestroyed(not_null<Ui::InputField*> field);
 | 
						|
	int findField(not_null<Ui::InputField*> field) const;
 | 
						|
 | 
						|
	not_null<QWidget*> _outer;
 | 
						|
	not_null<Ui::VerticalLayout*> _container;
 | 
						|
	int _position = 0;
 | 
						|
	std::vector<Option> _list;
 | 
						|
	std::set<Option, std::less<>> _destroyed;
 | 
						|
	rpl::variable<bool> _valid = false;
 | 
						|
	rpl::variable<int> _usedCount = 0;
 | 
						|
	rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
 | 
						|
	rpl::event_stream<> _backspaceInFront;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
void InitField(
 | 
						|
		not_null<QWidget*> container,
 | 
						|
		not_null<Ui::InputField*> field) {
 | 
						|
	field->setInstantReplaces(Ui::InstantReplaces::Default());
 | 
						|
	field->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
 | 
						|
	Ui::Emoji::SuggestionsController::Init(container, field);
 | 
						|
}
 | 
						|
 | 
						|
not_null<Ui::FlatLabel*> CreateWarningLabel(
 | 
						|
		not_null<QWidget*> parent,
 | 
						|
		not_null<Ui::InputField*> field,
 | 
						|
		int valueLimit,
 | 
						|
		int warnLimit) {
 | 
						|
	const auto result = Ui::CreateChild<Ui::FlatLabel>(
 | 
						|
		parent.get(),
 | 
						|
		QString(),
 | 
						|
		Ui::FlatLabel::InitType::Simple,
 | 
						|
		st::createPollWarning);
 | 
						|
	result->setAttribute(Qt::WA_TransparentForMouseEvents);
 | 
						|
	QObject::connect(field, &Ui::InputField::changed, [=] {
 | 
						|
		Ui::PostponeCall(crl::guard(field, [=] {
 | 
						|
			const auto length = field->getLastText().size();
 | 
						|
			const auto value = valueLimit - length;
 | 
						|
			const auto shown = (value < warnLimit)
 | 
						|
				&& (field->height() > st::createPollOptionField.heightMin);
 | 
						|
			result->setRichText((value >= 0)
 | 
						|
				? QString::number(value)
 | 
						|
				: textcmdLink(1, QString::number(value)));
 | 
						|
			result->setVisible(shown);
 | 
						|
		}));
 | 
						|
	});
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void FocusAtEnd(not_null<Ui::InputField*> field) {
 | 
						|
	field->setFocus();
 | 
						|
	field->setCursorPosition(field->getLastText().size());
 | 
						|
	field->ensureCursorVisible();
 | 
						|
}
 | 
						|
 | 
						|
Options::Option Options::Option::Create(
 | 
						|
		not_null<QWidget*> outer,
 | 
						|
		not_null<Ui::VerticalLayout*> container,
 | 
						|
		int position) {
 | 
						|
	auto result = Option();
 | 
						|
	const auto field = container->insert(
 | 
						|
		position,
 | 
						|
		object_ptr<Ui::SlideWrap<Ui::InputField>>(
 | 
						|
			container,
 | 
						|
			object_ptr<Ui::InputField>(
 | 
						|
				container,
 | 
						|
				st::createPollOptionField,
 | 
						|
				Ui::InputField::Mode::NoNewlines,
 | 
						|
				langFactory(lng_polls_create_option_add))));
 | 
						|
	InitField(outer, field->entity());
 | 
						|
	field->entity()->setMaxLength(kOptionLimit + kErrorLimit);
 | 
						|
	result._field.reset(field);
 | 
						|
 | 
						|
	result.createShadow();
 | 
						|
	result.createRemove();
 | 
						|
	result.createWarning();
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
//bool Options::Option::hasShadow() const {
 | 
						|
//	return (_shadow != nullptr);
 | 
						|
//}
 | 
						|
 | 
						|
void Options::Option::createShadow() {
 | 
						|
	Expects(_field != nullptr);
 | 
						|
 | 
						|
	if (_shadow) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto value = Ui::CreateChild<Ui::PlainShadow>(field().get());
 | 
						|
	value->show();
 | 
						|
	field()->sizeValue(
 | 
						|
	) | rpl::start_with_next([=](QSize size) {
 | 
						|
		const auto left = st::createPollFieldPadding.left();
 | 
						|
		value->setGeometry(
 | 
						|
			left,
 | 
						|
			size.height() - st::lineWidth,
 | 
						|
			size.width() - left,
 | 
						|
			st::lineWidth);
 | 
						|
	}, value->lifetime());
 | 
						|
	_shadow.reset(value);
 | 
						|
}
 | 
						|
 | 
						|
//void Options::Option::destroyShadow() {
 | 
						|
//	_shadow = nullptr;
 | 
						|
//}
 | 
						|
 | 
						|
void Options::Option::createRemove() {
 | 
						|
	using namespace rpl::mappers;
 | 
						|
 | 
						|
	const auto field = this->field();
 | 
						|
	auto &lifetime = field->lifetime();
 | 
						|
 | 
						|
	const auto remove = Ui::CreateChild<Ui::CrossButton>(
 | 
						|
		field.get(),
 | 
						|
		st::createPollOptionRemove);
 | 
						|
	remove->hide(anim::type::instant);
 | 
						|
 | 
						|
	const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
 | 
						|
	_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
 | 
						|
 | 
						|
	QObject::connect(field, &Ui::InputField::changed, [=] {
 | 
						|
		// Don't capture 'this'! Because Option is a value type.
 | 
						|
		*toggle = !field->getLastText().isEmpty();
 | 
						|
	});
 | 
						|
	rpl::combine(
 | 
						|
		toggle->value(),
 | 
						|
		_removeAlways->value(),
 | 
						|
		_1 || _2
 | 
						|
	) | rpl::start_with_next([=](bool shown) {
 | 
						|
		remove->toggle(shown, anim::type::normal);
 | 
						|
	}, remove->lifetime());
 | 
						|
 | 
						|
	field->widthValue(
 | 
						|
	) | rpl::start_with_next([=](int width) {
 | 
						|
		remove->moveToRight(
 | 
						|
			st::createPollOptionRemovePosition.x(),
 | 
						|
			st::createPollOptionRemovePosition.y(),
 | 
						|
			width);
 | 
						|
	}, remove->lifetime());
 | 
						|
 | 
						|
	_remove.reset(remove);
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::createWarning() {
 | 
						|
	using namespace rpl::mappers;
 | 
						|
 | 
						|
	const auto field = this->field();
 | 
						|
	const auto warning = CreateWarningLabel(
 | 
						|
		field,
 | 
						|
		field,
 | 
						|
		kOptionLimit,
 | 
						|
		kWarnOptionLimit);
 | 
						|
	rpl::combine(
 | 
						|
		field->sizeValue(),
 | 
						|
		warning->sizeValue()
 | 
						|
	) | rpl::start_with_next([=](QSize size, QSize label) {
 | 
						|
		warning->moveToLeft(
 | 
						|
			(size.width()
 | 
						|
				- label.width()
 | 
						|
				- st::createPollWarningPosition.x()),
 | 
						|
			(size.height()
 | 
						|
				- label.height()
 | 
						|
				- st::createPollWarningPosition.y()),
 | 
						|
			size.width());
 | 
						|
	}, warning->lifetime());
 | 
						|
}
 | 
						|
 | 
						|
bool Options::Option::isEmpty() const {
 | 
						|
	return field()->getLastText().trimmed().isEmpty();
 | 
						|
}
 | 
						|
 | 
						|
bool Options::Option::isGood() const {
 | 
						|
	return !field()->getLastText().trimmed().isEmpty() && !isTooLong();
 | 
						|
}
 | 
						|
 | 
						|
bool Options::Option::isTooLong() const {
 | 
						|
	return (field()->getLastText().size() > kOptionLimit);
 | 
						|
}
 | 
						|
 | 
						|
bool Options::Option::hasFocus() const {
 | 
						|
	return field()->hasFocus();
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::setFocus() const {
 | 
						|
	FocusAtEnd(field());
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::clearValue() {
 | 
						|
	field()->setText(QString());
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::setPlaceholder() const {
 | 
						|
	field()->setPlaceholder(langFactory(lng_polls_create_option_add));
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::toggleRemoveAlways(bool toggled) {
 | 
						|
	*_removeAlways = toggled;
 | 
						|
}
 | 
						|
 | 
						|
not_null<Ui::InputField*> Options::Option::field() const {
 | 
						|
	return _field->entity();
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::removePlaceholder() const {
 | 
						|
	field()->setPlaceholder(nullptr);
 | 
						|
}
 | 
						|
 | 
						|
PollAnswer Options::Option::toPollAnswer(int index) const {
 | 
						|
	Expects(index >= 0 && index < kMaxOptionsCount);
 | 
						|
 | 
						|
	return PollAnswer{
 | 
						|
		field()->getLastText().trimmed(),
 | 
						|
		QByteArray(1, ('0' + index))
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
 | 
						|
	return _remove->clicks();
 | 
						|
}
 | 
						|
 | 
						|
Options::Options(
 | 
						|
	not_null<QWidget*> outer,
 | 
						|
	not_null<Ui::VerticalLayout*> container)
 | 
						|
: _outer(outer)
 | 
						|
, _container(container)
 | 
						|
, _position(_container->count()) {
 | 
						|
	checkLastOption();
 | 
						|
}
 | 
						|
 | 
						|
bool Options::full() const {
 | 
						|
	return (_list.size() == kMaxOptionsCount);
 | 
						|
}
 | 
						|
 | 
						|
bool Options::isValid() const {
 | 
						|
	return _valid.current();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<bool> Options::isValidChanged() const {
 | 
						|
	return _valid.changes();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<int> Options::usedCount() const {
 | 
						|
	return _usedCount.value();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {
 | 
						|
	return _scrollToWidget.events();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<> Options::backspaceInFront() const {
 | 
						|
	return _backspaceInFront.events();
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::show(anim::type animated) {
 | 
						|
	_field->hide(anim::type::instant);
 | 
						|
	_field->show(animated);
 | 
						|
}
 | 
						|
 | 
						|
void Options::Option::destroy(FnMut<void()> done) {
 | 
						|
	if (anim::Disabled() || _field->isHidden()) {
 | 
						|
		Ui::PostponeCall(std::move(done));
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	_field->hide(anim::type::normal);
 | 
						|
	App::CallDelayed(
 | 
						|
		st::slideWrapDuration * 2,
 | 
						|
		_field.get(),
 | 
						|
		std::move(done));
 | 
						|
}
 | 
						|
 | 
						|
std::vector<PollAnswer> Options::toPollAnswers() const {
 | 
						|
	auto result = std::vector<PollAnswer>();
 | 
						|
	result.reserve(_list.size());
 | 
						|
	auto counter = int(0);
 | 
						|
	const auto makeAnswer = [&](const Option &option) {
 | 
						|
		return option.toPollAnswer(counter++);
 | 
						|
	};
 | 
						|
	ranges::copy(
 | 
						|
		_list
 | 
						|
		| ranges::view::filter(&Option::isGood)
 | 
						|
		| ranges::view::transform(makeAnswer),
 | 
						|
		ranges::back_inserter(result));
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void Options::focusFirst() {
 | 
						|
	Expects(!_list.empty());
 | 
						|
 | 
						|
	_list.front().setFocus();
 | 
						|
}
 | 
						|
//
 | 
						|
//bool Options::correctShadows() const {
 | 
						|
//	// Last one should be without shadow if all options were used.
 | 
						|
//	const auto noShadow = ranges::find(
 | 
						|
//		_list,
 | 
						|
//		true,
 | 
						|
//		ranges::not_fn(&Option::hasShadow));
 | 
						|
//	return (noShadow == end(_list) - (full() ? 1 : 0));
 | 
						|
//}
 | 
						|
//
 | 
						|
//void Options::fixShadows() {
 | 
						|
//	if (correctShadows()) {
 | 
						|
//		return;
 | 
						|
//	}
 | 
						|
//	for (auto &option : _list) {
 | 
						|
//		option.createShadow();
 | 
						|
//	}
 | 
						|
//	if (full()) {
 | 
						|
//		_list.back().destroyShadow();
 | 
						|
//	}
 | 
						|
//}
 | 
						|
 | 
						|
void Options::removeEmptyTail() {
 | 
						|
	// Only one option at the end of options list can be empty.
 | 
						|
	// Remove all other trailing empty options.
 | 
						|
	// Only last empty and previous option have non-empty placeholders.
 | 
						|
	const auto focused = ranges::find_if(
 | 
						|
		_list,
 | 
						|
		&Option::hasFocus);
 | 
						|
	const auto end = _list.end();
 | 
						|
	const auto reversed = ranges::view::reverse(_list);
 | 
						|
	const auto emptyItem = ranges::find_if(
 | 
						|
		reversed,
 | 
						|
		ranges::not_fn(&Option::isEmpty)).base();
 | 
						|
	const auto focusLast = (focused > emptyItem) && (focused < end);
 | 
						|
	if (emptyItem == end) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (focusLast) {
 | 
						|
		emptyItem->setFocus();
 | 
						|
	}
 | 
						|
	for (auto i = emptyItem + 1; i != end; ++i) {
 | 
						|
		destroy(std::move(*i));
 | 
						|
	}
 | 
						|
	_list.erase(emptyItem + 1, end);
 | 
						|
	fixAfterErase();
 | 
						|
}
 | 
						|
 | 
						|
void Options::destroy(Option &&option) {
 | 
						|
	const auto field = option.field();
 | 
						|
	option.destroy([=] { removeDestroyed(field); });
 | 
						|
	_destroyed.emplace(std::move(option));
 | 
						|
}
 | 
						|
 | 
						|
void Options::fixAfterErase() {
 | 
						|
	Expects(!_list.empty());
 | 
						|
 | 
						|
	const auto last = _list.end() - 1;
 | 
						|
	last->setPlaceholder();
 | 
						|
	last->toggleRemoveAlways(false);
 | 
						|
	if (last != begin(_list)) {
 | 
						|
		(last - 1)->setPlaceholder();
 | 
						|
		(last - 1)->toggleRemoveAlways(false);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Options::addEmptyOption() {
 | 
						|
	if (full()) {
 | 
						|
		return;
 | 
						|
	} else if (!_list.empty() && _list.back().isEmpty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	if (_list.size() > 1) {
 | 
						|
		(_list.end() - 2)->removePlaceholder();
 | 
						|
		(_list.end() - 2)->toggleRemoveAlways(true);
 | 
						|
	}
 | 
						|
	_list.push_back(Option::Create(
 | 
						|
		_outer,
 | 
						|
		_container,
 | 
						|
		_position + _list.size() + _destroyed.size()));
 | 
						|
	const auto field = _list.back().field();
 | 
						|
	QObject::connect(field, &Ui::InputField::submitted, [=] {
 | 
						|
		const auto index = findField(field);
 | 
						|
		if (_list[index].isGood() && index + 1 < _list.size()) {
 | 
						|
			_list[index + 1].setFocus();
 | 
						|
		}
 | 
						|
	});
 | 
						|
	QObject::connect(field, &Ui::InputField::changed, [=] {
 | 
						|
		Ui::PostponeCall(crl::guard(field, [=] {
 | 
						|
			validateState();
 | 
						|
		}));
 | 
						|
	});
 | 
						|
	QObject::connect(field, &Ui::InputField::focused, [=] {
 | 
						|
		_scrollToWidget.fire_copy(field);
 | 
						|
	});
 | 
						|
	Core::InstallEventFilter(field, [=](not_null<QEvent*> event) {
 | 
						|
		if (event->type() != QEvent::KeyPress
 | 
						|
			|| !field->getLastText().isEmpty()) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
		const auto key = static_cast<QKeyEvent*>(event.get())->key();
 | 
						|
		if (key != Qt::Key_Backspace) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		const auto index = findField(field);
 | 
						|
		if (index > 0) {
 | 
						|
			_list[index - 1].setFocus();
 | 
						|
		} else {
 | 
						|
			_backspaceInFront.fire({});
 | 
						|
		}
 | 
						|
		return true;
 | 
						|
	});
 | 
						|
 | 
						|
	_list.back().removeClicks(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		Ui::PostponeCall(crl::guard(field, [=] {
 | 
						|
			Expects(!_list.empty());
 | 
						|
 | 
						|
			const auto item = begin(_list) + findField(field);
 | 
						|
			if (item == _list.end() - 1) {
 | 
						|
				item->clearValue();
 | 
						|
				return;
 | 
						|
			}
 | 
						|
			if (item->hasFocus()) {
 | 
						|
				(item + 1)->setFocus();
 | 
						|
			}
 | 
						|
			destroy(std::move(*item));
 | 
						|
			_list.erase(item);
 | 
						|
			fixAfterErase();
 | 
						|
			validateState();
 | 
						|
		}));
 | 
						|
	}, field->lifetime());
 | 
						|
 | 
						|
	_list.back().show((_list.size() == 1)
 | 
						|
		? anim::type::instant
 | 
						|
		: anim::type::normal);
 | 
						|
	//fixShadows();
 | 
						|
}
 | 
						|
 | 
						|
void Options::removeDestroyed(not_null<Ui::InputField*> field) {
 | 
						|
	_destroyed.erase(_destroyed.find(field));
 | 
						|
}
 | 
						|
 | 
						|
void Options::validateState() {
 | 
						|
	checkLastOption();
 | 
						|
	_valid = (ranges::count_if(_list, &Option::isGood) > 1)
 | 
						|
		&& (ranges::find_if(_list, &Option::isTooLong) == end(_list));
 | 
						|
	const auto lastEmpty = !_list.empty() && _list.back().isEmpty();
 | 
						|
	_usedCount = _list.size() - (lastEmpty ? 1 : 0);
 | 
						|
}
 | 
						|
 | 
						|
int Options::findField(not_null<Ui::InputField*> field) const {
 | 
						|
	const auto result = ranges::find(
 | 
						|
		_list,
 | 
						|
		field,
 | 
						|
		&Option::field) - begin(_list);
 | 
						|
 | 
						|
	Ensures(result >= 0 && result < _list.size());
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void Options::checkLastOption() {
 | 
						|
	removeEmptyTail();
 | 
						|
	addEmptyOption();
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
CreatePollBox::CreatePollBox(QWidget*) {
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<PollData> CreatePollBox::submitRequests() const {
 | 
						|
	return _submitRequests.events();
 | 
						|
}
 | 
						|
 | 
						|
void CreatePollBox::setInnerFocus() {
 | 
						|
	_setInnerFocus();
 | 
						|
}
 | 
						|
 | 
						|
void CreatePollBox::submitFailed(const QString &error) {
 | 
						|
	Ui::Toast::Show(error);
 | 
						|
}
 | 
						|
 | 
						|
not_null<Ui::InputField*> CreatePollBox::setupQuestion(
 | 
						|
		not_null<Ui::VerticalLayout*> container) {
 | 
						|
	using namespace Settings;
 | 
						|
 | 
						|
	AddSubsectionTitle(container, lng_polls_create_question);
 | 
						|
	const auto question = container->add(
 | 
						|
		object_ptr<Ui::InputField>(
 | 
						|
			container,
 | 
						|
			st::createPollField,
 | 
						|
			Ui::InputField::Mode::MultiLine,
 | 
						|
			langFactory(lng_polls_create_question_placeholder)),
 | 
						|
		st::createPollFieldPadding);
 | 
						|
	InitField(getDelegate()->outerContainer(), question);
 | 
						|
	question->setMaxLength(kQuestionLimit + kErrorLimit);
 | 
						|
 | 
						|
	const auto warning = CreateWarningLabel(
 | 
						|
		container,
 | 
						|
		question,
 | 
						|
		kQuestionLimit,
 | 
						|
		kWarnQuestionLimit);
 | 
						|
	rpl::combine(
 | 
						|
		question->geometryValue(),
 | 
						|
		warning->sizeValue()
 | 
						|
	) | rpl::start_with_next([=](QRect geometry, QSize label) {
 | 
						|
		warning->moveToLeft(
 | 
						|
			(container->width()
 | 
						|
				- label.width()
 | 
						|
				- st::createPollWarningPosition.x()),
 | 
						|
			(geometry.y()
 | 
						|
				- st::createPollFieldPadding.top()
 | 
						|
				- st::settingsSubsectionTitlePadding.bottom()
 | 
						|
				- st::settingsSubsectionTitle.style.font->height
 | 
						|
				+ st::settingsSubsectionTitle.style.font->ascent
 | 
						|
				- st::createPollWarning.style.font->ascent),
 | 
						|
			geometry.width());
 | 
						|
	}, warning->lifetime());
 | 
						|
 | 
						|
	return question;
 | 
						|
}
 | 
						|
 | 
						|
object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
 | 
						|
	using namespace Settings;
 | 
						|
 | 
						|
	const auto id = rand_value<uint64>();
 | 
						|
	const auto valid = lifetime().make_state<rpl::event_stream<bool>>();
 | 
						|
 | 
						|
	auto result = object_ptr<Ui::VerticalLayout>(this);
 | 
						|
	const auto container = result.data();
 | 
						|
 | 
						|
	const auto question = setupQuestion(container);
 | 
						|
	AddDivider(container);
 | 
						|
	AddSkip(container);
 | 
						|
	AddSubsectionTitle(container, lng_polls_create_options);
 | 
						|
	const auto options = lifetime().make_state<Options>(
 | 
						|
		getDelegate()->outerContainer(),
 | 
						|
		container);
 | 
						|
	auto limit = options->usedCount() | rpl::after_next([=](int count) {
 | 
						|
		setCloseByEscape(!count);
 | 
						|
		setCloseByOutsideClick(!count);
 | 
						|
	}) | rpl::map([=](int count) {
 | 
						|
		return (count < kMaxOptionsCount)
 | 
						|
			? lng_polls_create_limit(lt_count, kMaxOptionsCount - count)
 | 
						|
			: lang(lng_polls_create_maximum);
 | 
						|
	}) | rpl::after_next([=] {
 | 
						|
		container->resizeToWidth(container->widthNoMargins());
 | 
						|
	});
 | 
						|
	container->add(
 | 
						|
		object_ptr<Ui::FlatLabel>(
 | 
						|
			container,
 | 
						|
			std::move(limit),
 | 
						|
			st::createPollLimitLabel),
 | 
						|
		st::createPollLimitPadding);
 | 
						|
 | 
						|
	const auto isValidQuestion = [=] {
 | 
						|
		const auto text = question->getLastText().trimmed();
 | 
						|
		return !text.isEmpty() && (text.size() <= kQuestionLimit);
 | 
						|
	};
 | 
						|
 | 
						|
	connect(question, &Ui::InputField::submitted, [=] {
 | 
						|
		if (isValidQuestion()) {
 | 
						|
			options->focusFirst();
 | 
						|
		}
 | 
						|
	});
 | 
						|
 | 
						|
	_setInnerFocus = [=] {
 | 
						|
		question->setFocusFast();
 | 
						|
	};
 | 
						|
 | 
						|
	const auto collectResult = [=] {
 | 
						|
		auto result = PollData(id);
 | 
						|
		result.question = question->getLastText().trimmed();
 | 
						|
		result.answers = options->toPollAnswers();
 | 
						|
		return result;
 | 
						|
	};
 | 
						|
	const auto updateValid = [=] {
 | 
						|
		valid->fire(isValidQuestion() && options->isValid());
 | 
						|
	};
 | 
						|
	connect(question, &Ui::InputField::changed, [=] {
 | 
						|
		updateValid();
 | 
						|
	});
 | 
						|
	valid->events_starting_with(
 | 
						|
		false
 | 
						|
	) | rpl::distinct_until_changed(
 | 
						|
	) | rpl::start_with_next([=](bool valid) {
 | 
						|
		clearButtons();
 | 
						|
		if (valid) {
 | 
						|
			addButton(
 | 
						|
				langFactory(lng_polls_create_button),
 | 
						|
				[=] { _submitRequests.fire(collectResult()); });
 | 
						|
		}
 | 
						|
		addButton(langFactory(lng_cancel), [=] { closeBox(); });
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	options->isValidChanged(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		updateValid();
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	options->scrollToWidget(
 | 
						|
	) | rpl::start_with_next([=](not_null<QWidget*> widget) {
 | 
						|
		scrollToWidget(widget);
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	options->backspaceInFront(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		FocusAtEnd(question);
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	return std::move(result);
 | 
						|
}
 | 
						|
 | 
						|
void CreatePollBox::prepare() {
 | 
						|
	setTitle(langFactory(lng_polls_create_title));
 | 
						|
 | 
						|
	const auto inner = setInnerWidget(setupContent());
 | 
						|
 | 
						|
	setDimensionsToContent(st::boxWideWidth, inner);
 | 
						|
}
 |