| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256 |
- /*
- 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 "base/call_delayed.h"
- #include "base/event_filter.h"
- #include "base/random.h"
- #include "base/unique_qptr.h"
- #include "chat_helpers/emoji_suggestions_widget.h"
- #include "chat_helpers/message_field.h"
- #include "chat_helpers/tabbed_panel.h"
- #include "chat_helpers/tabbed_selector.h"
- #include "core/application.h"
- #include "core/core_settings.h"
- #include "data/data_poll.h"
- #include "data/data_user.h"
- #include "data/stickers/data_custom_emoji.h"
- #include "history/view/history_view_schedule_box.h"
- #include "lang/lang_keys.h"
- #include "main/main_session.h"
- #include "menu/menu_send.h"
- #include "ui/controls/emoji_button.h"
- #include "ui/controls/emoji_button_factory.h"
- #include "ui/rect.h"
- #include "ui/text/text_utilities.h"
- #include "ui/toast/toast.h"
- #include "ui/vertical_list.h"
- #include "ui/widgets/buttons.h"
- #include "ui/widgets/checkbox.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/widgets/labels.h"
- #include "ui/widgets/shadow.h"
- #include "ui/wrap/fade_wrap.h"
- #include "ui/wrap/slide_wrap.h"
- #include "ui/wrap/vertical_layout.h"
- #include "ui/ui_utility.h"
- #include "window/window_session_controller.h"
- #include "styles/style_boxes.h"
- #include "styles/style_chat_helpers.h" // defaultComposeFiles.
- #include "styles/style_layers.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 kSolutionLimit = 200;
- constexpr auto kWarnSolutionLimit = 60;
- constexpr auto kErrorLimit = 99;
- class Options {
- public:
- Options(
- not_null<Ui::BoxContent*> box,
- not_null<Ui::VerticalLayout*> container,
- not_null<Window::SessionController*> controller,
- ChatHelpers::TabbedPanel *emojiPanel,
- bool chooseCorrectEnabled);
- [[nodiscard]] bool hasOptions() const;
- [[nodiscard]] bool isValid() const;
- [[nodiscard]] bool hasCorrect() const;
- [[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
- void focusFirst();
- void enableChooseCorrect(bool enabled);
- [[nodiscard]] rpl::producer<int> usedCount() const;
- [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
- [[nodiscard]] rpl::producer<> backspaceInFront() const;
- [[nodiscard]] rpl::producer<> tabbed() const;
- private:
- class Option {
- public:
- Option(
- not_null<QWidget*> outer,
- not_null<Ui::VerticalLayout*> container,
- not_null<Main::Session*> session,
- int position,
- std::shared_ptr<Ui::RadiobuttonGroup> group);
- Option(const Option &other) = delete;
- Option &operator=(const Option &other) = delete;
- void toggleRemoveAlways(bool toggled);
- void enableChooseCorrect(
- std::shared_ptr<Ui::RadiobuttonGroup> group);
- void show(anim::type animated);
- void destroy(FnMut<void()> done);
- [[nodiscard]] bool hasShadow() const;
- void createShadow();
- void destroyShadow();
- [[nodiscard]] bool isEmpty() const;
- [[nodiscard]] bool isGood() const;
- [[nodiscard]] bool isTooLong() const;
- [[nodiscard]] bool isCorrect() 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;
- private:
- void createRemove();
- void createWarning();
- void toggleCorrectSpace(bool visible);
- void updateFieldGeometry();
- base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
- not_null<Ui::RpWidget*> _content;
- base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct;
- Ui::Animations::Simple _correctShown;
- bool _hasCorrect = false;
- Ui::InputField *_field = nullptr;
- 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(std::unique_ptr<Option> option);
- void removeDestroyed(not_null<Option*> field);
- int findField(not_null<Ui::InputField*> field) const;
- [[nodiscard]] auto createChooseCorrectGroup()
- -> std::shared_ptr<Ui::RadiobuttonGroup>;
- not_null<Ui::BoxContent*> _box;
- not_null<Ui::VerticalLayout*> _container;
- const not_null<Window::SessionController*> _controller;
- ChatHelpers::TabbedPanel * const _emojiPanel;
- std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
- int _position = 0;
- std::vector<std::unique_ptr<Option>> _list;
- std::vector<std::unique_ptr<Option>> _destroyed;
- rpl::variable<int> _usedCount = 0;
- bool _hasOptions = false;
- bool _isValid = false;
- bool _hasCorrect = false;
- rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
- rpl::event_stream<> _backspaceInFront;
- rpl::event_stream<> _tabbed;
- rpl::lifetime _emojiPanelLifetime;
- };
- void InitField(
- not_null<QWidget*> container,
- not_null<Ui::InputField*> field,
- not_null<Main::Session*> session) {
- field->setInstantReplaces(Ui::InstantReplaces::Default());
- field->setInstantReplacesEnabled(
- Core::App().settings().replaceEmojiValue());
- auto options = Ui::Emoji::SuggestionsController::Options();
- options.suggestExactFirstWord = false;
- Ui::Emoji::SuggestionsController::Init(
- container,
- field,
- session,
- options);
- }
- 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(),
- st::createPollWarning);
- result->setAttribute(Qt::WA_TransparentForMouseEvents);
- field->changes(
- ) | rpl::start_with_next([=] {
- 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);
- if (value >= 0) {
- result->setText(QString::number(value));
- } else {
- constexpr auto kMinus = QChar(0x2212);
- result->setMarkedText(Ui::Text::Colorized(
- kMinus + QString::number(std::abs(value))));
- }
- result->setVisible(shown);
- }));
- }, field->lifetime());
- return result;
- }
- void FocusAtEnd(not_null<Ui::InputField*> field) {
- field->setFocus();
- field->setCursorPosition(field->getLastText().size());
- field->ensureCursorVisible();
- }
- Options::Option::Option(
- not_null<QWidget*> outer,
- not_null<Ui::VerticalLayout*> container,
- not_null<Main::Session*> session,
- int position,
- std::shared_ptr<Ui::RadiobuttonGroup> group)
- : _wrap(container->insert(
- position,
- object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
- container,
- object_ptr<Ui::RpWidget>(container))))
- , _content(_wrap->entity())
- , _field(
- Ui::CreateChild<Ui::InputField>(
- _content.get(),
- session->user()->isPremium()
- ? st::createPollOptionFieldPremium
- : st::createPollOptionField,
- Ui::InputField::Mode::NoNewlines,
- tr::lng_polls_create_option_add())) {
- InitField(outer, _field, session);
- _field->setMaxLength(kOptionLimit + kErrorLimit);
- _field->show();
- _field->customTab(true);
- _wrap->hide(anim::type::instant);
- _content->widthValue(
- ) | rpl::start_with_next([=] {
- updateFieldGeometry();
- }, _field->lifetime());
- _field->heightValue(
- ) | rpl::start_with_next([=](int height) {
- _content->resize(_content->width(), height);
- }, _field->lifetime());
- _field->changes(
- ) | rpl::start_with_next([=] {
- Ui::PostponeCall(crl::guard(_field, [=] {
- if (_hasCorrect) {
- _correct->toggle(isGood(), anim::type::normal);
- }
- }));
- }, _field->lifetime());
- createShadow();
- createRemove();
- createWarning();
- enableChooseCorrect(group);
- _correctShown.stop();
- if (_correct) {
- _correct->finishAnimating();
- }
- updateFieldGeometry();
- }
- bool Options::Option::hasShadow() const {
- return (_shadow != nullptr);
- }
- void Options::Option::createShadow() {
- Expects(_content != nullptr);
- if (_shadow) {
- return;
- }
- _shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
- _shadow->show();
- field()->sizeValue(
- ) | rpl::start_with_next([=](QSize size) {
- const auto left = st::createPollFieldPadding.left();
- _shadow->setGeometry(
- left,
- size.height() - st::lineWidth,
- size.width() - left,
- st::lineWidth);
- }, _shadow->lifetime());
- }
- 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->show(anim::type::instant);
- const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
- _removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
- field->changes(
- ) | rpl::start_with_next([field, toggle] {
- // Don't capture 'this'! Because Option is a value type.
- *toggle = !field->getLastText().isEmpty();
- }, field->lifetime());
- #if 0
- rpl::combine(
- toggle->value(),
- _removeAlways->value(),
- _1 || _2
- ) | rpl::start_with_next([=](bool shown) {
- remove->toggle(shown, anim::type::normal);
- }, remove->lifetime());
- #endif
- 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::isCorrect() const {
- return isGood() && _correct && _correct->entity()->Checkbox::checked();
- }
- 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(tr::lng_polls_create_option_add());
- }
- void Options::Option::toggleRemoveAlways(bool toggled) {
- *_removeAlways = toggled;
- }
- void Options::Option::enableChooseCorrect(
- std::shared_ptr<Ui::RadiobuttonGroup> group) {
- if (!group) {
- if (_correct) {
- _hasCorrect = false;
- _correct->hide(anim::type::normal);
- toggleCorrectSpace(false);
- }
- return;
- }
- static auto Index = 0;
- const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>(
- _content.get(),
- object_ptr<Ui::Radiobutton>(
- _content.get(),
- group,
- ++Index,
- QString(),
- st::defaultCheckbox));
- button->entity()->resize(
- button->entity()->height(),
- button->entity()->height());
- button->hide(anim::type::instant);
- _content->sizeValue(
- ) | rpl::start_with_next([=](QSize size) {
- const auto left = st::createPollFieldPadding.left();
- button->moveToLeft(
- left,
- (size.height() - button->heightNoMargins()) / 2);
- }, button->lifetime());
- _correct.reset(button);
- _hasCorrect = true;
- if (isGood()) {
- _correct->show(anim::type::normal);
- } else {
- _correct->hide(anim::type::instant);
- }
- toggleCorrectSpace(true);
- }
- void Options::Option::toggleCorrectSpace(bool visible) {
- _correctShown.start(
- [=] { updateFieldGeometry(); },
- visible ? 0. : 1.,
- visible ? 1. : 0.,
- st::fadeWrapDuration);
- }
- void Options::Option::updateFieldGeometry() {
- const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.);
- const auto skip = st::defaultRadio.diameter
- + st::defaultCheckbox.textPosition.x();
- const auto left = anim::interpolate(0, skip, shown);
- _field->resizeToWidth(_content->width() - left);
- _field->moveToLeft(left, 0);
- }
- not_null<Ui::InputField*> Options::Option::field() const {
- return _field;
- }
- void Options::Option::removePlaceholder() const {
- field()->setPlaceholder(rpl::single(QString()));
- }
- PollAnswer Options::Option::toPollAnswer(int index) const {
- Expects(index >= 0 && index < kMaxOptionsCount);
- const auto text = field()->getTextWithTags();
- auto result = PollAnswer{
- TextWithEntities{
- .text = text.text,
- .entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
- },
- QByteArray(1, ('0' + index)),
- };
- TextUtilities::Trim(result.text);
- result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
- return result;
- }
- rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
- return _remove->clicks();
- }
- Options::Options(
- not_null<Ui::BoxContent*> box,
- not_null<Ui::VerticalLayout*> container,
- not_null<Window::SessionController*> controller,
- ChatHelpers::TabbedPanel *emojiPanel,
- bool chooseCorrectEnabled)
- : _box(box)
- , _container(container)
- , _controller(controller)
- , _emojiPanel(emojiPanel)
- , _chooseCorrectGroup(chooseCorrectEnabled
- ? createChooseCorrectGroup()
- : nullptr)
- , _position(_container->count()) {
- checkLastOption();
- }
- bool Options::full() const {
- return (_list.size() == kMaxOptionsCount);
- }
- bool Options::hasOptions() const {
- return _hasOptions;
- }
- bool Options::isValid() const {
- return _isValid;
- }
- bool Options::hasCorrect() const {
- return _hasCorrect;
- }
- 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();
- }
- rpl::producer<> Options::tabbed() const {
- return _tabbed.events();
- }
- void Options::Option::show(anim::type animated) {
- _wrap->show(animated);
- }
- void Options::Option::destroy(FnMut<void()> done) {
- if (anim::Disabled() || _wrap->isHidden()) {
- Ui::PostponeCall(std::move(done));
- return;
- }
- _wrap->hide(anim::type::normal);
- base::call_delayed(
- st::slideWrapDuration * 2,
- _content.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 std::unique_ptr<Option> &option) {
- return option->toPollAnswer(counter++);
- };
- ranges::copy(
- _list
- | ranges::views::filter(&Option::isGood)
- | ranges::views::transform(makeAnswer),
- ranges::back_inserter(result));
- return result;
- }
- void Options::focusFirst() {
- Expects(!_list.empty());
- _list.front()->setFocus();
- }
- std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {
- auto result = std::make_shared<Ui::RadiobuttonGroup>(0);
- result->setChangedCallback([=](int) {
- validateState();
- });
- return result;
- }
- void Options::enableChooseCorrect(bool enabled) {
- _chooseCorrectGroup = enabled
- ? createChooseCorrectGroup()
- : nullptr;
- for (auto &option : _list) {
- option->enableChooseCorrect(_chooseCorrectGroup);
- }
- validateState();
- }
- bool Options::correctShadows() const {
- // Last one should be without shadow.
- const auto noShadow = ranges::find(
- _list,
- true,
- ranges::not_fn(&Option::hasShadow));
- return (noShadow == end(_list) - 1);
- }
- void Options::fixShadows() {
- if (correctShadows()) {
- return;
- }
- for (auto &option : _list) {
- option->createShadow();
- }
- _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::views::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(std::unique_ptr<Option> option) {
- const auto value = option.get();
- option->destroy([=] { removeDestroyed(value); });
- _destroyed.push_back(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);
- }
- fixShadows();
- }
- 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(std::make_unique<Option>(
- _box,
- _container,
- &_controller->session(),
- _position + _list.size() + _destroyed.size(),
- _chooseCorrectGroup));
- const auto field = _list.back()->field();
- if (const auto emojiPanel = _emojiPanel) {
- const auto emojiToggle = Ui::AddEmojiToggleToField(
- field,
- _box,
- _controller,
- emojiPanel,
- QPoint(
- -st::createPollOptionFieldPremium.textMargins.right(),
- st::createPollOptionEmojiPositionSkip));
- emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) {
- if (!shown) {
- return;
- }
- _emojiPanelLifetime.destroy();
- emojiPanel->selector()->emojiChosen(
- ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
- if (field->hasFocus()) {
- Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
- }
- }, _emojiPanelLifetime);
- emojiPanel->selector()->customEmojiChosen(
- ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
- if (field->hasFocus()) {
- Data::InsertCustomEmoji(field, data.document);
- }
- }, _emojiPanelLifetime);
- }, emojiToggle->lifetime());
- }
- field->submits(
- ) | rpl::start_with_next([=] {
- const auto index = findField(field);
- if (_list[index]->isGood() && index + 1 < _list.size()) {
- _list[index + 1]->setFocus();
- }
- }, field->lifetime());
- field->changes(
- ) | rpl::start_with_next([=] {
- Ui::PostponeCall(crl::guard(field, [=] {
- validateState();
- }));
- }, field->lifetime());
- field->focusedChanges(
- ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] {
- _scrollToWidget.fire_copy(field);
- }, field->lifetime());
- field->tabbed(
- ) | rpl::start_with_next([=] {
- const auto index = findField(field);
- if (index + 1 < _list.size()) {
- _list[index + 1]->setFocus();
- } else {
- _tabbed.fire({});
- }
- }, field->lifetime());
- base::install_event_filter(field, [=](not_null<QEvent*> event) {
- if (event->type() != QEvent::KeyPress
- || !field->getLastText().isEmpty()) {
- return base::EventFilterResult::Continue;
- }
- const auto key = static_cast<QKeyEvent*>(event.get())->key();
- if (key != Qt::Key_Backspace) {
- return base::EventFilterResult::Continue;
- }
- const auto index = findField(field);
- if (index > 0) {
- _list[index - 1]->setFocus();
- } else {
- _backspaceInFront.fire({});
- }
- return base::EventFilterResult::Cancel;
- });
- _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<Option*> option) {
- const auto i = ranges::find(
- _destroyed,
- option.get(),
- &std::unique_ptr<Option>::get);
- Assert(i != end(_destroyed));
- _destroyed.erase(i);
- }
- void Options::validateState() {
- checkLastOption();
- _hasOptions = (ranges::count_if(_list, &Option::isGood) > 1);
- _isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);
- _hasCorrect = ranges::any_of(_list, &Option::isCorrect);
- 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*,
- not_null<Window::SessionController*> controller,
- PollData::Flags chosen,
- PollData::Flags disabled,
- rpl::producer<int> starsRequired,
- Api::SendType sendType,
- SendMenu::Details sendMenuDetails)
- : _controller(controller)
- , _chosen(chosen)
- , _disabled(disabled)
- , _sendType(sendType)
- , _sendMenuDetails([result = sendMenuDetails] { return result; })
- , _starsRequired(std::move(starsRequired)) {
- }
- rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
- return _submitRequests.events();
- }
- void CreatePollBox::setInnerFocus() {
- _setInnerFocus();
- }
- void CreatePollBox::submitFailed(const QString &error) {
- showToast(error);
- }
- not_null<Ui::InputField*> CreatePollBox::setupQuestion(
- not_null<Ui::VerticalLayout*> container) {
- using namespace Settings;
- const auto session = &_controller->session();
- const auto isPremium = session->user()->isPremium();
- Ui::AddSubsectionTitle(container, tr::lng_polls_create_question());
- const auto question = container->add(
- object_ptr<Ui::InputField>(
- container,
- st::createPollField,
- Ui::InputField::Mode::MultiLine,
- tr::lng_polls_create_question_placeholder()),
- st::createPollFieldPadding
- + (isPremium
- ? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0)
- : QMargins()));
- InitField(getDelegate()->outerContainer(), question, session);
- question->setMaxLength(kQuestionLimit + kErrorLimit);
- question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
- question->customTab(true);
- if (isPremium) {
- using Selector = ChatHelpers::TabbedSelector;
- const auto outer = getDelegate()->outerContainer();
- _emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
- outer,
- _controller,
- object_ptr<Selector>(
- nullptr,
- _controller->uiShow(),
- Window::GifPauseReason::Layer,
- Selector::Mode::EmojiOnly));
- const auto emojiPanel = _emojiPanel.get();
- emojiPanel->setDesiredHeightValues(
- 1.,
- st::emojiPanMinHeight / 2,
- st::emojiPanMinHeight);
- emojiPanel->hide();
- emojiPanel->selector()->setCurrentPeer(session->user());
- const auto emojiToggle = Ui::AddEmojiToggleToField(
- question,
- this,
- _controller,
- emojiPanel,
- st::createPollOptionFieldPremiumEmojiPosition);
- emojiPanel->selector()->emojiChosen(
- ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
- if (question->hasFocus()) {
- Ui::InsertEmojiAtCursor(question->textCursor(), data.emoji);
- }
- }, emojiToggle->lifetime());
- emojiPanel->selector()->customEmojiChosen(
- ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
- if (question->hasFocus()) {
- Data::InsertCustomEmoji(question, data.document);
- }
- }, emojiToggle->lifetime());
- }
- 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::defaultSubsectionTitlePadding.bottom()
- - st::defaultSubsectionTitle.style.font->height
- + st::defaultSubsectionTitle.style.font->ascent
- - st::createPollWarning.style.font->ascent),
- geometry.width());
- }, warning->lifetime());
- return question;
- }
- not_null<Ui::InputField*> CreatePollBox::setupSolution(
- not_null<Ui::VerticalLayout*> container,
- rpl::producer<bool> shown) {
- using namespace Settings;
- const auto outer = container->add(
- object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
- container,
- object_ptr<Ui::VerticalLayout>(container))
- )->setDuration(0)->toggleOn(std::move(shown));
- const auto inner = outer->entity();
- const auto session = &_controller->session();
- Ui::AddSkip(inner);
- Ui::AddSubsectionTitle(inner, tr::lng_polls_solution_title());
- const auto solution = inner->add(
- object_ptr<Ui::InputField>(
- inner,
- st::createPollSolutionField,
- Ui::InputField::Mode::MultiLine,
- tr::lng_polls_solution_placeholder()),
- st::createPollFieldPadding);
- InitField(getDelegate()->outerContainer(), solution, session);
- solution->setMaxLength(kSolutionLimit + kErrorLimit);
- solution->setInstantReplaces(Ui::InstantReplaces::Default());
- solution->setInstantReplacesEnabled(
- Core::App().settings().replaceEmojiValue());
- solution->setMarkdownReplacesEnabled(rpl::single(
- Ui::MarkdownEnabledState{ Ui::MarkdownEnabled{ {
- Ui::InputField::kTagBold,
- Ui::InputField::kTagItalic,
- Ui::InputField::kTagUnderline,
- Ui::InputField::kTagStrikeOut,
- Ui::InputField::kTagCode,
- Ui::InputField::kTagSpoiler,
- } } }
- ));
- solution->setEditLinkCallback(
- DefaultEditLinkCallback(_controller->uiShow(), solution));
- solution->customTab(true);
- const auto warning = CreateWarningLabel(
- inner,
- solution,
- kSolutionLimit,
- kWarnSolutionLimit);
- rpl::combine(
- solution->geometryValue(),
- warning->sizeValue()
- ) | rpl::start_with_next([=](QRect geometry, QSize label) {
- warning->moveToLeft(
- (inner->width()
- - label.width()
- - st::createPollWarningPosition.x()),
- (geometry.y()
- - st::createPollFieldPadding.top()
- - st::defaultSubsectionTitlePadding.bottom()
- - st::defaultSubsectionTitle.style.font->height
- + st::defaultSubsectionTitle.style.font->ascent
- - st::createPollWarning.style.font->ascent),
- geometry.width());
- }, warning->lifetime());
- inner->add(
- object_ptr<Ui::FlatLabel>(
- inner,
- tr::lng_polls_solution_about(),
- st::boxDividerLabel),
- st::createPollFieldTitlePadding);
- return solution;
- }
- object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
- using namespace Settings;
- const auto id = base::RandomValue<uint64>();
- const auto error = lifetime().make_state<Errors>(Error::Question);
- auto result = object_ptr<Ui::VerticalLayout>(this);
- const auto container = result.data();
- const auto question = setupQuestion(container);
- Ui::AddDivider(container);
- Ui::AddSkip(container);
- container->add(
- object_ptr<Ui::FlatLabel>(
- container,
- tr::lng_polls_create_options(),
- st::defaultSubsectionTitle),
- st::createPollFieldTitlePadding);
- const auto options = lifetime().make_state<Options>(
- this,
- container,
- _controller,
- _emojiPanel ? _emojiPanel.get() : nullptr,
- (_chosen & PollData::Flag::Quiz));
- auto limit = options->usedCount() | rpl::after_next([=](int count) {
- setCloseByEscape(!count);
- setCloseByOutsideClick(!count);
- }) | rpl::map([=](int count) {
- return (count < kMaxOptionsCount)
- ? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count)
- : tr::lng_polls_create_maximum(tr::now);
- }) | rpl::after_next([=] {
- container->resizeToWidth(container->widthNoMargins());
- });
- container->add(
- object_ptr<Ui::DividerLabel>(
- container,
- object_ptr<Ui::FlatLabel>(
- container,
- std::move(limit),
- st::boxDividerLabel),
- st::createPollLimitPadding));
- question->tabbed(
- ) | rpl::start_with_next([=] {
- options->focusFirst();
- }, question->lifetime());
- Ui::AddSkip(container);
- Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings());
- const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes))
- ? container->add(
- object_ptr<Ui::Checkbox>(
- container,
- tr::lng_polls_create_anonymous(tr::now),
- !(_chosen & PollData::Flag::PublicVotes),
- st::defaultCheckbox),
- st::createPollCheckboxMargin)
- : nullptr;
- const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
- || !(_disabled & PollData::Flag::Quiz);
- const auto multiple = hasMultiple
- ? container->add(
- object_ptr<Ui::Checkbox>(
- container,
- tr::lng_polls_create_multiple_choice(tr::now),
- (_chosen & PollData::Flag::MultiChoice),
- st::defaultCheckbox),
- st::createPollCheckboxMargin)
- : nullptr;
- const auto quiz = container->add(
- object_ptr<Ui::Checkbox>(
- container,
- tr::lng_polls_create_quiz_mode(tr::now),
- (_chosen & PollData::Flag::Quiz),
- st::defaultCheckbox),
- st::createPollCheckboxMargin);
- const auto solution = setupSolution(
- container,
- rpl::single(quiz->checked()) | rpl::then(quiz->checkedChanges()));
- options->tabbed(
- ) | rpl::start_with_next([=] {
- if (quiz->checked()) {
- solution->setFocus();
- } else {
- question->setFocus();
- }
- }, question->lifetime());
- solution->tabbed(
- ) | rpl::start_with_next([=] {
- question->setFocus();
- }, solution->lifetime());
- quiz->setDisabled(_disabled & PollData::Flag::Quiz);
- if (multiple) {
- multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
- || (_chosen & PollData::Flag::Quiz));
- multiple->events(
- ) | rpl::filter([=](not_null<QEvent*> e) {
- return (e->type() == QEvent::MouseButtonPress)
- && quiz->checked();
- }) | rpl::start_with_next([show = uiShow()] {
- show->showToast(tr::lng_polls_create_one_answer(tr::now));
- }, multiple->lifetime());
- }
- using namespace rpl::mappers;
- quiz->checkedChanges(
- ) | rpl::start_with_next([=](bool checked) {
- if (multiple) {
- if (checked && multiple->checked()) {
- multiple->setChecked(false);
- }
- multiple->setDisabled(checked
- || (_disabled & PollData::Flag::MultiChoice));
- }
- options->enableChooseCorrect(checked);
- }, quiz->lifetime());
- const auto isValidQuestion = [=] {
- const auto text = question->getLastText().trimmed();
- return !text.isEmpty() && (text.size() <= kQuestionLimit);
- };
- question->submits(
- ) | rpl::start_with_next([=] {
- if (isValidQuestion()) {
- options->focusFirst();
- }
- }, question->lifetime());
- _setInnerFocus = [=] {
- question->setFocusFast();
- };
- const auto collectResult = [=] {
- const auto textWithTags = question->getTextWithTags();
- using Flag = PollData::Flag;
- auto result = PollData(&_controller->session().data(), id);
- result.question.text = textWithTags.text;
- result.question.entities = TextUtilities::ConvertTextTagsToEntities(
- textWithTags.tags);
- TextUtilities::Trim(result.question);
- result.answers = options->toPollAnswers();
- const auto solutionWithTags = quiz->checked()
- ? solution->getTextWithAppliedMarkdown()
- : TextWithTags();
- result.solution = TextWithEntities{
- solutionWithTags.text,
- TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
- };
- const auto publicVotes = (anonymous && !anonymous->checked());
- const auto multiChoice = (multiple && multiple->checked());
- result.setFlags(Flag(0)
- | (publicVotes ? Flag::PublicVotes : Flag(0))
- | (multiChoice ? Flag::MultiChoice : Flag(0))
- | (quiz->checked() ? Flag::Quiz : Flag(0)));
- return result;
- };
- const auto collectError = [=] {
- if (isValidQuestion()) {
- *error &= ~Error::Question;
- } else {
- *error |= Error::Question;
- }
- if (!options->hasOptions()) {
- *error |= Error::Options;
- } else if (!options->isValid()) {
- *error |= Error::Other;
- } else {
- *error &= ~(Error::Options | Error::Other);
- }
- if (quiz->checked() && !options->hasCorrect()) {
- *error |= Error::Correct;
- } else {
- *error &= ~Error::Correct;
- }
- if (quiz->checked()
- && solution->getLastText().trimmed().size() > kSolutionLimit) {
- *error |= Error::Solution;
- } else {
- *error &= ~Error::Solution;
- }
- };
- const auto showError = [show = uiShow()](
- tr::phrase<> text) {
- show->showToast(text(tr::now));
- };
- const auto send = [=](Api::SendOptions sendOptions) {
- collectError();
- if (*error & Error::Question) {
- showError(tr::lng_polls_choose_question);
- question->setFocus();
- } else if (*error & Error::Options) {
- showError(tr::lng_polls_choose_answers);
- options->focusFirst();
- } else if (*error & Error::Correct) {
- showError(tr::lng_polls_choose_correct);
- } else if (*error & Error::Solution) {
- solution->showError();
- } else if (!*error) {
- _submitRequests.fire({ collectResult(), sendOptions });
- }
- };
- const auto sendAction = SendMenu::DefaultCallback(
- _controller->uiShow(),
- crl::guard(this, send));
- options->scrollToWidget(
- ) | rpl::start_with_next([=](not_null<QWidget*> widget) {
- scrollToWidget(widget);
- }, lifetime());
- options->backspaceInFront(
- ) | rpl::start_with_next([=] {
- FocusAtEnd(question);
- }, lifetime());
- const auto isNormal = (_sendType == Api::SendType::Normal);
- const auto schedule = [=] {
- sendAction(
- { .type = SendMenu::ActionType::Schedule },
- _sendMenuDetails());
- };
- const auto submit = addButton(
- tr::lng_polls_create_button(),
- [=] { isNormal ? send({}) : schedule(); });
- submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
- ? tr::lng_polls_create_button()
- : tr::lng_schedule_button()));
- const auto sendMenuDetails = [=] {
- collectError();
- return (*error) ? SendMenu::Details() : _sendMenuDetails();
- };
- SendMenu::SetupMenuAndShortcuts(
- submit.data(),
- _controller->uiShow(),
- sendMenuDetails,
- sendAction);
- addButton(tr::lng_cancel(), [=] { closeBox(); });
- return result;
- }
- void CreatePollBox::prepare() {
- setTitle(tr::lng_polls_create_title());
- const auto inner = setInnerWidget(setupContent());
- setDimensionsToContent(st::boxWideWidth, inner);
- }
|