create_poll_box.cpp 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "boxes/create_poll_box.h"
  8. #include "base/call_delayed.h"
  9. #include "base/event_filter.h"
  10. #include "base/random.h"
  11. #include "base/unique_qptr.h"
  12. #include "chat_helpers/emoji_suggestions_widget.h"
  13. #include "chat_helpers/message_field.h"
  14. #include "chat_helpers/tabbed_panel.h"
  15. #include "chat_helpers/tabbed_selector.h"
  16. #include "core/application.h"
  17. #include "core/core_settings.h"
  18. #include "data/data_poll.h"
  19. #include "data/data_user.h"
  20. #include "data/stickers/data_custom_emoji.h"
  21. #include "history/view/history_view_schedule_box.h"
  22. #include "lang/lang_keys.h"
  23. #include "main/main_session.h"
  24. #include "menu/menu_send.h"
  25. #include "ui/controls/emoji_button.h"
  26. #include "ui/controls/emoji_button_factory.h"
  27. #include "ui/rect.h"
  28. #include "ui/text/text_utilities.h"
  29. #include "ui/toast/toast.h"
  30. #include "ui/vertical_list.h"
  31. #include "ui/widgets/buttons.h"
  32. #include "ui/widgets/checkbox.h"
  33. #include "ui/widgets/fields/input_field.h"
  34. #include "ui/widgets/labels.h"
  35. #include "ui/widgets/shadow.h"
  36. #include "ui/wrap/fade_wrap.h"
  37. #include "ui/wrap/slide_wrap.h"
  38. #include "ui/wrap/vertical_layout.h"
  39. #include "ui/ui_utility.h"
  40. #include "window/window_session_controller.h"
  41. #include "styles/style_boxes.h"
  42. #include "styles/style_chat_helpers.h" // defaultComposeFiles.
  43. #include "styles/style_layers.h"
  44. #include "styles/style_settings.h"
  45. namespace {
  46. constexpr auto kQuestionLimit = 255;
  47. constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
  48. constexpr auto kOptionLimit = 100;
  49. constexpr auto kWarnQuestionLimit = 80;
  50. constexpr auto kWarnOptionLimit = 30;
  51. constexpr auto kSolutionLimit = 200;
  52. constexpr auto kWarnSolutionLimit = 60;
  53. constexpr auto kErrorLimit = 99;
  54. class Options {
  55. public:
  56. Options(
  57. not_null<Ui::BoxContent*> box,
  58. not_null<Ui::VerticalLayout*> container,
  59. not_null<Window::SessionController*> controller,
  60. ChatHelpers::TabbedPanel *emojiPanel,
  61. bool chooseCorrectEnabled);
  62. [[nodiscard]] bool hasOptions() const;
  63. [[nodiscard]] bool isValid() const;
  64. [[nodiscard]] bool hasCorrect() const;
  65. [[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
  66. void focusFirst();
  67. void enableChooseCorrect(bool enabled);
  68. [[nodiscard]] rpl::producer<int> usedCount() const;
  69. [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
  70. [[nodiscard]] rpl::producer<> backspaceInFront() const;
  71. [[nodiscard]] rpl::producer<> tabbed() const;
  72. private:
  73. class Option {
  74. public:
  75. Option(
  76. not_null<QWidget*> outer,
  77. not_null<Ui::VerticalLayout*> container,
  78. not_null<Main::Session*> session,
  79. int position,
  80. std::shared_ptr<Ui::RadiobuttonGroup> group);
  81. Option(const Option &other) = delete;
  82. Option &operator=(const Option &other) = delete;
  83. void toggleRemoveAlways(bool toggled);
  84. void enableChooseCorrect(
  85. std::shared_ptr<Ui::RadiobuttonGroup> group);
  86. void show(anim::type animated);
  87. void destroy(FnMut<void()> done);
  88. [[nodiscard]] bool hasShadow() const;
  89. void createShadow();
  90. void destroyShadow();
  91. [[nodiscard]] bool isEmpty() const;
  92. [[nodiscard]] bool isGood() const;
  93. [[nodiscard]] bool isTooLong() const;
  94. [[nodiscard]] bool isCorrect() const;
  95. [[nodiscard]] bool hasFocus() const;
  96. void setFocus() const;
  97. void clearValue();
  98. void setPlaceholder() const;
  99. void removePlaceholder() const;
  100. not_null<Ui::InputField*> field() const;
  101. [[nodiscard]] PollAnswer toPollAnswer(int index) const;
  102. [[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
  103. private:
  104. void createRemove();
  105. void createWarning();
  106. void toggleCorrectSpace(bool visible);
  107. void updateFieldGeometry();
  108. base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
  109. not_null<Ui::RpWidget*> _content;
  110. base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct;
  111. Ui::Animations::Simple _correctShown;
  112. bool _hasCorrect = false;
  113. Ui::InputField *_field = nullptr;
  114. base::unique_qptr<Ui::PlainShadow> _shadow;
  115. base::unique_qptr<Ui::CrossButton> _remove;
  116. rpl::variable<bool> *_removeAlways = nullptr;
  117. };
  118. [[nodiscard]] bool full() const;
  119. [[nodiscard]] bool correctShadows() const;
  120. void fixShadows();
  121. void removeEmptyTail();
  122. void addEmptyOption();
  123. void checkLastOption();
  124. void validateState();
  125. void fixAfterErase();
  126. void destroy(std::unique_ptr<Option> option);
  127. void removeDestroyed(not_null<Option*> field);
  128. int findField(not_null<Ui::InputField*> field) const;
  129. [[nodiscard]] auto createChooseCorrectGroup()
  130. -> std::shared_ptr<Ui::RadiobuttonGroup>;
  131. not_null<Ui::BoxContent*> _box;
  132. not_null<Ui::VerticalLayout*> _container;
  133. const not_null<Window::SessionController*> _controller;
  134. ChatHelpers::TabbedPanel * const _emojiPanel;
  135. std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
  136. int _position = 0;
  137. std::vector<std::unique_ptr<Option>> _list;
  138. std::vector<std::unique_ptr<Option>> _destroyed;
  139. rpl::variable<int> _usedCount = 0;
  140. bool _hasOptions = false;
  141. bool _isValid = false;
  142. bool _hasCorrect = false;
  143. rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
  144. rpl::event_stream<> _backspaceInFront;
  145. rpl::event_stream<> _tabbed;
  146. rpl::lifetime _emojiPanelLifetime;
  147. };
  148. void InitField(
  149. not_null<QWidget*> container,
  150. not_null<Ui::InputField*> field,
  151. not_null<Main::Session*> session) {
  152. field->setInstantReplaces(Ui::InstantReplaces::Default());
  153. field->setInstantReplacesEnabled(
  154. Core::App().settings().replaceEmojiValue());
  155. auto options = Ui::Emoji::SuggestionsController::Options();
  156. options.suggestExactFirstWord = false;
  157. Ui::Emoji::SuggestionsController::Init(
  158. container,
  159. field,
  160. session,
  161. options);
  162. }
  163. not_null<Ui::FlatLabel*> CreateWarningLabel(
  164. not_null<QWidget*> parent,
  165. not_null<Ui::InputField*> field,
  166. int valueLimit,
  167. int warnLimit) {
  168. const auto result = Ui::CreateChild<Ui::FlatLabel>(
  169. parent.get(),
  170. QString(),
  171. st::createPollWarning);
  172. result->setAttribute(Qt::WA_TransparentForMouseEvents);
  173. field->changes(
  174. ) | rpl::start_with_next([=] {
  175. Ui::PostponeCall(crl::guard(field, [=] {
  176. const auto length = field->getLastText().size();
  177. const auto value = valueLimit - length;
  178. const auto shown = (value < warnLimit)
  179. && (field->height() > st::createPollOptionField.heightMin);
  180. if (value >= 0) {
  181. result->setText(QString::number(value));
  182. } else {
  183. constexpr auto kMinus = QChar(0x2212);
  184. result->setMarkedText(Ui::Text::Colorized(
  185. kMinus + QString::number(std::abs(value))));
  186. }
  187. result->setVisible(shown);
  188. }));
  189. }, field->lifetime());
  190. return result;
  191. }
  192. void FocusAtEnd(not_null<Ui::InputField*> field) {
  193. field->setFocus();
  194. field->setCursorPosition(field->getLastText().size());
  195. field->ensureCursorVisible();
  196. }
  197. Options::Option::Option(
  198. not_null<QWidget*> outer,
  199. not_null<Ui::VerticalLayout*> container,
  200. not_null<Main::Session*> session,
  201. int position,
  202. std::shared_ptr<Ui::RadiobuttonGroup> group)
  203. : _wrap(container->insert(
  204. position,
  205. object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
  206. container,
  207. object_ptr<Ui::RpWidget>(container))))
  208. , _content(_wrap->entity())
  209. , _field(
  210. Ui::CreateChild<Ui::InputField>(
  211. _content.get(),
  212. session->user()->isPremium()
  213. ? st::createPollOptionFieldPremium
  214. : st::createPollOptionField,
  215. Ui::InputField::Mode::NoNewlines,
  216. tr::lng_polls_create_option_add())) {
  217. InitField(outer, _field, session);
  218. _field->setMaxLength(kOptionLimit + kErrorLimit);
  219. _field->show();
  220. _field->customTab(true);
  221. _wrap->hide(anim::type::instant);
  222. _content->widthValue(
  223. ) | rpl::start_with_next([=] {
  224. updateFieldGeometry();
  225. }, _field->lifetime());
  226. _field->heightValue(
  227. ) | rpl::start_with_next([=](int height) {
  228. _content->resize(_content->width(), height);
  229. }, _field->lifetime());
  230. _field->changes(
  231. ) | rpl::start_with_next([=] {
  232. Ui::PostponeCall(crl::guard(_field, [=] {
  233. if (_hasCorrect) {
  234. _correct->toggle(isGood(), anim::type::normal);
  235. }
  236. }));
  237. }, _field->lifetime());
  238. createShadow();
  239. createRemove();
  240. createWarning();
  241. enableChooseCorrect(group);
  242. _correctShown.stop();
  243. if (_correct) {
  244. _correct->finishAnimating();
  245. }
  246. updateFieldGeometry();
  247. }
  248. bool Options::Option::hasShadow() const {
  249. return (_shadow != nullptr);
  250. }
  251. void Options::Option::createShadow() {
  252. Expects(_content != nullptr);
  253. if (_shadow) {
  254. return;
  255. }
  256. _shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
  257. _shadow->show();
  258. field()->sizeValue(
  259. ) | rpl::start_with_next([=](QSize size) {
  260. const auto left = st::createPollFieldPadding.left();
  261. _shadow->setGeometry(
  262. left,
  263. size.height() - st::lineWidth,
  264. size.width() - left,
  265. st::lineWidth);
  266. }, _shadow->lifetime());
  267. }
  268. void Options::Option::destroyShadow() {
  269. _shadow = nullptr;
  270. }
  271. void Options::Option::createRemove() {
  272. using namespace rpl::mappers;
  273. const auto field = this->field();
  274. auto &lifetime = field->lifetime();
  275. const auto remove = Ui::CreateChild<Ui::CrossButton>(
  276. field.get(),
  277. st::createPollOptionRemove);
  278. remove->show(anim::type::instant);
  279. const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
  280. _removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
  281. field->changes(
  282. ) | rpl::start_with_next([field, toggle] {
  283. // Don't capture 'this'! Because Option is a value type.
  284. *toggle = !field->getLastText().isEmpty();
  285. }, field->lifetime());
  286. #if 0
  287. rpl::combine(
  288. toggle->value(),
  289. _removeAlways->value(),
  290. _1 || _2
  291. ) | rpl::start_with_next([=](bool shown) {
  292. remove->toggle(shown, anim::type::normal);
  293. }, remove->lifetime());
  294. #endif
  295. field->widthValue(
  296. ) | rpl::start_with_next([=](int width) {
  297. remove->moveToRight(
  298. st::createPollOptionRemovePosition.x(),
  299. st::createPollOptionRemovePosition.y(),
  300. width);
  301. }, remove->lifetime());
  302. _remove.reset(remove);
  303. }
  304. void Options::Option::createWarning() {
  305. using namespace rpl::mappers;
  306. const auto field = this->field();
  307. const auto warning = CreateWarningLabel(
  308. field,
  309. field,
  310. kOptionLimit,
  311. kWarnOptionLimit);
  312. rpl::combine(
  313. field->sizeValue(),
  314. warning->sizeValue()
  315. ) | rpl::start_with_next([=](QSize size, QSize label) {
  316. warning->moveToLeft(
  317. (size.width()
  318. - label.width()
  319. - st::createPollWarningPosition.x()),
  320. (size.height()
  321. - label.height()
  322. - st::createPollWarningPosition.y()),
  323. size.width());
  324. }, warning->lifetime());
  325. }
  326. bool Options::Option::isEmpty() const {
  327. return field()->getLastText().trimmed().isEmpty();
  328. }
  329. bool Options::Option::isGood() const {
  330. return !field()->getLastText().trimmed().isEmpty() && !isTooLong();
  331. }
  332. bool Options::Option::isTooLong() const {
  333. return (field()->getLastText().size() > kOptionLimit);
  334. }
  335. bool Options::Option::isCorrect() const {
  336. return isGood() && _correct && _correct->entity()->Checkbox::checked();
  337. }
  338. bool Options::Option::hasFocus() const {
  339. return field()->hasFocus();
  340. }
  341. void Options::Option::setFocus() const {
  342. FocusAtEnd(field());
  343. }
  344. void Options::Option::clearValue() {
  345. field()->setText(QString());
  346. }
  347. void Options::Option::setPlaceholder() const {
  348. field()->setPlaceholder(tr::lng_polls_create_option_add());
  349. }
  350. void Options::Option::toggleRemoveAlways(bool toggled) {
  351. *_removeAlways = toggled;
  352. }
  353. void Options::Option::enableChooseCorrect(
  354. std::shared_ptr<Ui::RadiobuttonGroup> group) {
  355. if (!group) {
  356. if (_correct) {
  357. _hasCorrect = false;
  358. _correct->hide(anim::type::normal);
  359. toggleCorrectSpace(false);
  360. }
  361. return;
  362. }
  363. static auto Index = 0;
  364. const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>(
  365. _content.get(),
  366. object_ptr<Ui::Radiobutton>(
  367. _content.get(),
  368. group,
  369. ++Index,
  370. QString(),
  371. st::defaultCheckbox));
  372. button->entity()->resize(
  373. button->entity()->height(),
  374. button->entity()->height());
  375. button->hide(anim::type::instant);
  376. _content->sizeValue(
  377. ) | rpl::start_with_next([=](QSize size) {
  378. const auto left = st::createPollFieldPadding.left();
  379. button->moveToLeft(
  380. left,
  381. (size.height() - button->heightNoMargins()) / 2);
  382. }, button->lifetime());
  383. _correct.reset(button);
  384. _hasCorrect = true;
  385. if (isGood()) {
  386. _correct->show(anim::type::normal);
  387. } else {
  388. _correct->hide(anim::type::instant);
  389. }
  390. toggleCorrectSpace(true);
  391. }
  392. void Options::Option::toggleCorrectSpace(bool visible) {
  393. _correctShown.start(
  394. [=] { updateFieldGeometry(); },
  395. visible ? 0. : 1.,
  396. visible ? 1. : 0.,
  397. st::fadeWrapDuration);
  398. }
  399. void Options::Option::updateFieldGeometry() {
  400. const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.);
  401. const auto skip = st::defaultRadio.diameter
  402. + st::defaultCheckbox.textPosition.x();
  403. const auto left = anim::interpolate(0, skip, shown);
  404. _field->resizeToWidth(_content->width() - left);
  405. _field->moveToLeft(left, 0);
  406. }
  407. not_null<Ui::InputField*> Options::Option::field() const {
  408. return _field;
  409. }
  410. void Options::Option::removePlaceholder() const {
  411. field()->setPlaceholder(rpl::single(QString()));
  412. }
  413. PollAnswer Options::Option::toPollAnswer(int index) const {
  414. Expects(index >= 0 && index < kMaxOptionsCount);
  415. const auto text = field()->getTextWithTags();
  416. auto result = PollAnswer{
  417. TextWithEntities{
  418. .text = text.text,
  419. .entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
  420. },
  421. QByteArray(1, ('0' + index)),
  422. };
  423. TextUtilities::Trim(result.text);
  424. result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
  425. return result;
  426. }
  427. rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
  428. return _remove->clicks();
  429. }
  430. Options::Options(
  431. not_null<Ui::BoxContent*> box,
  432. not_null<Ui::VerticalLayout*> container,
  433. not_null<Window::SessionController*> controller,
  434. ChatHelpers::TabbedPanel *emojiPanel,
  435. bool chooseCorrectEnabled)
  436. : _box(box)
  437. , _container(container)
  438. , _controller(controller)
  439. , _emojiPanel(emojiPanel)
  440. , _chooseCorrectGroup(chooseCorrectEnabled
  441. ? createChooseCorrectGroup()
  442. : nullptr)
  443. , _position(_container->count()) {
  444. checkLastOption();
  445. }
  446. bool Options::full() const {
  447. return (_list.size() == kMaxOptionsCount);
  448. }
  449. bool Options::hasOptions() const {
  450. return _hasOptions;
  451. }
  452. bool Options::isValid() const {
  453. return _isValid;
  454. }
  455. bool Options::hasCorrect() const {
  456. return _hasCorrect;
  457. }
  458. rpl::producer<int> Options::usedCount() const {
  459. return _usedCount.value();
  460. }
  461. rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {
  462. return _scrollToWidget.events();
  463. }
  464. rpl::producer<> Options::backspaceInFront() const {
  465. return _backspaceInFront.events();
  466. }
  467. rpl::producer<> Options::tabbed() const {
  468. return _tabbed.events();
  469. }
  470. void Options::Option::show(anim::type animated) {
  471. _wrap->show(animated);
  472. }
  473. void Options::Option::destroy(FnMut<void()> done) {
  474. if (anim::Disabled() || _wrap->isHidden()) {
  475. Ui::PostponeCall(std::move(done));
  476. return;
  477. }
  478. _wrap->hide(anim::type::normal);
  479. base::call_delayed(
  480. st::slideWrapDuration * 2,
  481. _content.get(),
  482. std::move(done));
  483. }
  484. std::vector<PollAnswer> Options::toPollAnswers() const {
  485. auto result = std::vector<PollAnswer>();
  486. result.reserve(_list.size());
  487. auto counter = int(0);
  488. const auto makeAnswer = [&](const std::unique_ptr<Option> &option) {
  489. return option->toPollAnswer(counter++);
  490. };
  491. ranges::copy(
  492. _list
  493. | ranges::views::filter(&Option::isGood)
  494. | ranges::views::transform(makeAnswer),
  495. ranges::back_inserter(result));
  496. return result;
  497. }
  498. void Options::focusFirst() {
  499. Expects(!_list.empty());
  500. _list.front()->setFocus();
  501. }
  502. std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {
  503. auto result = std::make_shared<Ui::RadiobuttonGroup>(0);
  504. result->setChangedCallback([=](int) {
  505. validateState();
  506. });
  507. return result;
  508. }
  509. void Options::enableChooseCorrect(bool enabled) {
  510. _chooseCorrectGroup = enabled
  511. ? createChooseCorrectGroup()
  512. : nullptr;
  513. for (auto &option : _list) {
  514. option->enableChooseCorrect(_chooseCorrectGroup);
  515. }
  516. validateState();
  517. }
  518. bool Options::correctShadows() const {
  519. // Last one should be without shadow.
  520. const auto noShadow = ranges::find(
  521. _list,
  522. true,
  523. ranges::not_fn(&Option::hasShadow));
  524. return (noShadow == end(_list) - 1);
  525. }
  526. void Options::fixShadows() {
  527. if (correctShadows()) {
  528. return;
  529. }
  530. for (auto &option : _list) {
  531. option->createShadow();
  532. }
  533. _list.back()->destroyShadow();
  534. }
  535. void Options::removeEmptyTail() {
  536. // Only one option at the end of options list can be empty.
  537. // Remove all other trailing empty options.
  538. // Only last empty and previous option have non-empty placeholders.
  539. const auto focused = ranges::find_if(
  540. _list,
  541. &Option::hasFocus);
  542. const auto end = _list.end();
  543. const auto reversed = ranges::views::reverse(_list);
  544. const auto emptyItem = ranges::find_if(
  545. reversed,
  546. ranges::not_fn(&Option::isEmpty)).base();
  547. const auto focusLast = (focused > emptyItem) && (focused < end);
  548. if (emptyItem == end) {
  549. return;
  550. }
  551. if (focusLast) {
  552. (*emptyItem)->setFocus();
  553. }
  554. for (auto i = emptyItem + 1; i != end; ++i) {
  555. destroy(std::move(*i));
  556. }
  557. _list.erase(emptyItem + 1, end);
  558. fixAfterErase();
  559. }
  560. void Options::destroy(std::unique_ptr<Option> option) {
  561. const auto value = option.get();
  562. option->destroy([=] { removeDestroyed(value); });
  563. _destroyed.push_back(std::move(option));
  564. }
  565. void Options::fixAfterErase() {
  566. Expects(!_list.empty());
  567. const auto last = _list.end() - 1;
  568. (*last)->setPlaceholder();
  569. (*last)->toggleRemoveAlways(false);
  570. if (last != begin(_list)) {
  571. (*(last - 1))->setPlaceholder();
  572. (*(last - 1))->toggleRemoveAlways(false);
  573. }
  574. fixShadows();
  575. }
  576. void Options::addEmptyOption() {
  577. if (full()) {
  578. return;
  579. } else if (!_list.empty() && _list.back()->isEmpty()) {
  580. return;
  581. }
  582. if (_list.size() > 1) {
  583. (*(_list.end() - 2))->removePlaceholder();
  584. (*(_list.end() - 2))->toggleRemoveAlways(true);
  585. }
  586. _list.push_back(std::make_unique<Option>(
  587. _box,
  588. _container,
  589. &_controller->session(),
  590. _position + _list.size() + _destroyed.size(),
  591. _chooseCorrectGroup));
  592. const auto field = _list.back()->field();
  593. if (const auto emojiPanel = _emojiPanel) {
  594. const auto emojiToggle = Ui::AddEmojiToggleToField(
  595. field,
  596. _box,
  597. _controller,
  598. emojiPanel,
  599. QPoint(
  600. -st::createPollOptionFieldPremium.textMargins.right(),
  601. st::createPollOptionEmojiPositionSkip));
  602. emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) {
  603. if (!shown) {
  604. return;
  605. }
  606. _emojiPanelLifetime.destroy();
  607. emojiPanel->selector()->emojiChosen(
  608. ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
  609. if (field->hasFocus()) {
  610. Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
  611. }
  612. }, _emojiPanelLifetime);
  613. emojiPanel->selector()->customEmojiChosen(
  614. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  615. if (field->hasFocus()) {
  616. Data::InsertCustomEmoji(field, data.document);
  617. }
  618. }, _emojiPanelLifetime);
  619. }, emojiToggle->lifetime());
  620. }
  621. field->submits(
  622. ) | rpl::start_with_next([=] {
  623. const auto index = findField(field);
  624. if (_list[index]->isGood() && index + 1 < _list.size()) {
  625. _list[index + 1]->setFocus();
  626. }
  627. }, field->lifetime());
  628. field->changes(
  629. ) | rpl::start_with_next([=] {
  630. Ui::PostponeCall(crl::guard(field, [=] {
  631. validateState();
  632. }));
  633. }, field->lifetime());
  634. field->focusedChanges(
  635. ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] {
  636. _scrollToWidget.fire_copy(field);
  637. }, field->lifetime());
  638. field->tabbed(
  639. ) | rpl::start_with_next([=] {
  640. const auto index = findField(field);
  641. if (index + 1 < _list.size()) {
  642. _list[index + 1]->setFocus();
  643. } else {
  644. _tabbed.fire({});
  645. }
  646. }, field->lifetime());
  647. base::install_event_filter(field, [=](not_null<QEvent*> event) {
  648. if (event->type() != QEvent::KeyPress
  649. || !field->getLastText().isEmpty()) {
  650. return base::EventFilterResult::Continue;
  651. }
  652. const auto key = static_cast<QKeyEvent*>(event.get())->key();
  653. if (key != Qt::Key_Backspace) {
  654. return base::EventFilterResult::Continue;
  655. }
  656. const auto index = findField(field);
  657. if (index > 0) {
  658. _list[index - 1]->setFocus();
  659. } else {
  660. _backspaceInFront.fire({});
  661. }
  662. return base::EventFilterResult::Cancel;
  663. });
  664. _list.back()->removeClicks(
  665. ) | rpl::start_with_next([=] {
  666. Ui::PostponeCall(crl::guard(field, [=] {
  667. Expects(!_list.empty());
  668. const auto item = begin(_list) + findField(field);
  669. if (item == _list.end() - 1) {
  670. (*item)->clearValue();
  671. return;
  672. }
  673. if ((*item)->hasFocus()) {
  674. (*(item + 1))->setFocus();
  675. }
  676. destroy(std::move(*item));
  677. _list.erase(item);
  678. fixAfterErase();
  679. validateState();
  680. }));
  681. }, field->lifetime());
  682. _list.back()->show((_list.size() == 1)
  683. ? anim::type::instant
  684. : anim::type::normal);
  685. fixShadows();
  686. }
  687. void Options::removeDestroyed(not_null<Option*> option) {
  688. const auto i = ranges::find(
  689. _destroyed,
  690. option.get(),
  691. &std::unique_ptr<Option>::get);
  692. Assert(i != end(_destroyed));
  693. _destroyed.erase(i);
  694. }
  695. void Options::validateState() {
  696. checkLastOption();
  697. _hasOptions = (ranges::count_if(_list, &Option::isGood) > 1);
  698. _isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);
  699. _hasCorrect = ranges::any_of(_list, &Option::isCorrect);
  700. const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
  701. _usedCount = _list.size() - (lastEmpty ? 1 : 0);
  702. }
  703. int Options::findField(not_null<Ui::InputField*> field) const {
  704. const auto result = ranges::find(
  705. _list,
  706. field,
  707. &Option::field) - begin(_list);
  708. Ensures(result >= 0 && result < _list.size());
  709. return result;
  710. }
  711. void Options::checkLastOption() {
  712. removeEmptyTail();
  713. addEmptyOption();
  714. }
  715. } // namespace
  716. CreatePollBox::CreatePollBox(
  717. QWidget*,
  718. not_null<Window::SessionController*> controller,
  719. PollData::Flags chosen,
  720. PollData::Flags disabled,
  721. rpl::producer<int> starsRequired,
  722. Api::SendType sendType,
  723. SendMenu::Details sendMenuDetails)
  724. : _controller(controller)
  725. , _chosen(chosen)
  726. , _disabled(disabled)
  727. , _sendType(sendType)
  728. , _sendMenuDetails([result = sendMenuDetails] { return result; })
  729. , _starsRequired(std::move(starsRequired)) {
  730. }
  731. rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
  732. return _submitRequests.events();
  733. }
  734. void CreatePollBox::setInnerFocus() {
  735. _setInnerFocus();
  736. }
  737. void CreatePollBox::submitFailed(const QString &error) {
  738. showToast(error);
  739. }
  740. not_null<Ui::InputField*> CreatePollBox::setupQuestion(
  741. not_null<Ui::VerticalLayout*> container) {
  742. using namespace Settings;
  743. const auto session = &_controller->session();
  744. const auto isPremium = session->user()->isPremium();
  745. Ui::AddSubsectionTitle(container, tr::lng_polls_create_question());
  746. const auto question = container->add(
  747. object_ptr<Ui::InputField>(
  748. container,
  749. st::createPollField,
  750. Ui::InputField::Mode::MultiLine,
  751. tr::lng_polls_create_question_placeholder()),
  752. st::createPollFieldPadding
  753. + (isPremium
  754. ? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0)
  755. : QMargins()));
  756. InitField(getDelegate()->outerContainer(), question, session);
  757. question->setMaxLength(kQuestionLimit + kErrorLimit);
  758. question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
  759. question->customTab(true);
  760. if (isPremium) {
  761. using Selector = ChatHelpers::TabbedSelector;
  762. const auto outer = getDelegate()->outerContainer();
  763. _emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
  764. outer,
  765. _controller,
  766. object_ptr<Selector>(
  767. nullptr,
  768. _controller->uiShow(),
  769. Window::GifPauseReason::Layer,
  770. Selector::Mode::EmojiOnly));
  771. const auto emojiPanel = _emojiPanel.get();
  772. emojiPanel->setDesiredHeightValues(
  773. 1.,
  774. st::emojiPanMinHeight / 2,
  775. st::emojiPanMinHeight);
  776. emojiPanel->hide();
  777. emojiPanel->selector()->setCurrentPeer(session->user());
  778. const auto emojiToggle = Ui::AddEmojiToggleToField(
  779. question,
  780. this,
  781. _controller,
  782. emojiPanel,
  783. st::createPollOptionFieldPremiumEmojiPosition);
  784. emojiPanel->selector()->emojiChosen(
  785. ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
  786. if (question->hasFocus()) {
  787. Ui::InsertEmojiAtCursor(question->textCursor(), data.emoji);
  788. }
  789. }, emojiToggle->lifetime());
  790. emojiPanel->selector()->customEmojiChosen(
  791. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  792. if (question->hasFocus()) {
  793. Data::InsertCustomEmoji(question, data.document);
  794. }
  795. }, emojiToggle->lifetime());
  796. }
  797. const auto warning = CreateWarningLabel(
  798. container,
  799. question,
  800. kQuestionLimit,
  801. kWarnQuestionLimit);
  802. rpl::combine(
  803. question->geometryValue(),
  804. warning->sizeValue()
  805. ) | rpl::start_with_next([=](QRect geometry, QSize label) {
  806. warning->moveToLeft(
  807. (container->width()
  808. - label.width()
  809. - st::createPollWarningPosition.x()),
  810. (geometry.y()
  811. - st::createPollFieldPadding.top()
  812. - st::defaultSubsectionTitlePadding.bottom()
  813. - st::defaultSubsectionTitle.style.font->height
  814. + st::defaultSubsectionTitle.style.font->ascent
  815. - st::createPollWarning.style.font->ascent),
  816. geometry.width());
  817. }, warning->lifetime());
  818. return question;
  819. }
  820. not_null<Ui::InputField*> CreatePollBox::setupSolution(
  821. not_null<Ui::VerticalLayout*> container,
  822. rpl::producer<bool> shown) {
  823. using namespace Settings;
  824. const auto outer = container->add(
  825. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  826. container,
  827. object_ptr<Ui::VerticalLayout>(container))
  828. )->setDuration(0)->toggleOn(std::move(shown));
  829. const auto inner = outer->entity();
  830. const auto session = &_controller->session();
  831. Ui::AddSkip(inner);
  832. Ui::AddSubsectionTitle(inner, tr::lng_polls_solution_title());
  833. const auto solution = inner->add(
  834. object_ptr<Ui::InputField>(
  835. inner,
  836. st::createPollSolutionField,
  837. Ui::InputField::Mode::MultiLine,
  838. tr::lng_polls_solution_placeholder()),
  839. st::createPollFieldPadding);
  840. InitField(getDelegate()->outerContainer(), solution, session);
  841. solution->setMaxLength(kSolutionLimit + kErrorLimit);
  842. solution->setInstantReplaces(Ui::InstantReplaces::Default());
  843. solution->setInstantReplacesEnabled(
  844. Core::App().settings().replaceEmojiValue());
  845. solution->setMarkdownReplacesEnabled(rpl::single(
  846. Ui::MarkdownEnabledState{ Ui::MarkdownEnabled{ {
  847. Ui::InputField::kTagBold,
  848. Ui::InputField::kTagItalic,
  849. Ui::InputField::kTagUnderline,
  850. Ui::InputField::kTagStrikeOut,
  851. Ui::InputField::kTagCode,
  852. Ui::InputField::kTagSpoiler,
  853. } } }
  854. ));
  855. solution->setEditLinkCallback(
  856. DefaultEditLinkCallback(_controller->uiShow(), solution));
  857. solution->customTab(true);
  858. const auto warning = CreateWarningLabel(
  859. inner,
  860. solution,
  861. kSolutionLimit,
  862. kWarnSolutionLimit);
  863. rpl::combine(
  864. solution->geometryValue(),
  865. warning->sizeValue()
  866. ) | rpl::start_with_next([=](QRect geometry, QSize label) {
  867. warning->moveToLeft(
  868. (inner->width()
  869. - label.width()
  870. - st::createPollWarningPosition.x()),
  871. (geometry.y()
  872. - st::createPollFieldPadding.top()
  873. - st::defaultSubsectionTitlePadding.bottom()
  874. - st::defaultSubsectionTitle.style.font->height
  875. + st::defaultSubsectionTitle.style.font->ascent
  876. - st::createPollWarning.style.font->ascent),
  877. geometry.width());
  878. }, warning->lifetime());
  879. inner->add(
  880. object_ptr<Ui::FlatLabel>(
  881. inner,
  882. tr::lng_polls_solution_about(),
  883. st::boxDividerLabel),
  884. st::createPollFieldTitlePadding);
  885. return solution;
  886. }
  887. object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
  888. using namespace Settings;
  889. const auto id = base::RandomValue<uint64>();
  890. const auto error = lifetime().make_state<Errors>(Error::Question);
  891. auto result = object_ptr<Ui::VerticalLayout>(this);
  892. const auto container = result.data();
  893. const auto question = setupQuestion(container);
  894. Ui::AddDivider(container);
  895. Ui::AddSkip(container);
  896. container->add(
  897. object_ptr<Ui::FlatLabel>(
  898. container,
  899. tr::lng_polls_create_options(),
  900. st::defaultSubsectionTitle),
  901. st::createPollFieldTitlePadding);
  902. const auto options = lifetime().make_state<Options>(
  903. this,
  904. container,
  905. _controller,
  906. _emojiPanel ? _emojiPanel.get() : nullptr,
  907. (_chosen & PollData::Flag::Quiz));
  908. auto limit = options->usedCount() | rpl::after_next([=](int count) {
  909. setCloseByEscape(!count);
  910. setCloseByOutsideClick(!count);
  911. }) | rpl::map([=](int count) {
  912. return (count < kMaxOptionsCount)
  913. ? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count)
  914. : tr::lng_polls_create_maximum(tr::now);
  915. }) | rpl::after_next([=] {
  916. container->resizeToWidth(container->widthNoMargins());
  917. });
  918. container->add(
  919. object_ptr<Ui::DividerLabel>(
  920. container,
  921. object_ptr<Ui::FlatLabel>(
  922. container,
  923. std::move(limit),
  924. st::boxDividerLabel),
  925. st::createPollLimitPadding));
  926. question->tabbed(
  927. ) | rpl::start_with_next([=] {
  928. options->focusFirst();
  929. }, question->lifetime());
  930. Ui::AddSkip(container);
  931. Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings());
  932. const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes))
  933. ? container->add(
  934. object_ptr<Ui::Checkbox>(
  935. container,
  936. tr::lng_polls_create_anonymous(tr::now),
  937. !(_chosen & PollData::Flag::PublicVotes),
  938. st::defaultCheckbox),
  939. st::createPollCheckboxMargin)
  940. : nullptr;
  941. const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
  942. || !(_disabled & PollData::Flag::Quiz);
  943. const auto multiple = hasMultiple
  944. ? container->add(
  945. object_ptr<Ui::Checkbox>(
  946. container,
  947. tr::lng_polls_create_multiple_choice(tr::now),
  948. (_chosen & PollData::Flag::MultiChoice),
  949. st::defaultCheckbox),
  950. st::createPollCheckboxMargin)
  951. : nullptr;
  952. const auto quiz = container->add(
  953. object_ptr<Ui::Checkbox>(
  954. container,
  955. tr::lng_polls_create_quiz_mode(tr::now),
  956. (_chosen & PollData::Flag::Quiz),
  957. st::defaultCheckbox),
  958. st::createPollCheckboxMargin);
  959. const auto solution = setupSolution(
  960. container,
  961. rpl::single(quiz->checked()) | rpl::then(quiz->checkedChanges()));
  962. options->tabbed(
  963. ) | rpl::start_with_next([=] {
  964. if (quiz->checked()) {
  965. solution->setFocus();
  966. } else {
  967. question->setFocus();
  968. }
  969. }, question->lifetime());
  970. solution->tabbed(
  971. ) | rpl::start_with_next([=] {
  972. question->setFocus();
  973. }, solution->lifetime());
  974. quiz->setDisabled(_disabled & PollData::Flag::Quiz);
  975. if (multiple) {
  976. multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
  977. || (_chosen & PollData::Flag::Quiz));
  978. multiple->events(
  979. ) | rpl::filter([=](not_null<QEvent*> e) {
  980. return (e->type() == QEvent::MouseButtonPress)
  981. && quiz->checked();
  982. }) | rpl::start_with_next([show = uiShow()] {
  983. show->showToast(tr::lng_polls_create_one_answer(tr::now));
  984. }, multiple->lifetime());
  985. }
  986. using namespace rpl::mappers;
  987. quiz->checkedChanges(
  988. ) | rpl::start_with_next([=](bool checked) {
  989. if (multiple) {
  990. if (checked && multiple->checked()) {
  991. multiple->setChecked(false);
  992. }
  993. multiple->setDisabled(checked
  994. || (_disabled & PollData::Flag::MultiChoice));
  995. }
  996. options->enableChooseCorrect(checked);
  997. }, quiz->lifetime());
  998. const auto isValidQuestion = [=] {
  999. const auto text = question->getLastText().trimmed();
  1000. return !text.isEmpty() && (text.size() <= kQuestionLimit);
  1001. };
  1002. question->submits(
  1003. ) | rpl::start_with_next([=] {
  1004. if (isValidQuestion()) {
  1005. options->focusFirst();
  1006. }
  1007. }, question->lifetime());
  1008. _setInnerFocus = [=] {
  1009. question->setFocusFast();
  1010. };
  1011. const auto collectResult = [=] {
  1012. const auto textWithTags = question->getTextWithTags();
  1013. using Flag = PollData::Flag;
  1014. auto result = PollData(&_controller->session().data(), id);
  1015. result.question.text = textWithTags.text;
  1016. result.question.entities = TextUtilities::ConvertTextTagsToEntities(
  1017. textWithTags.tags);
  1018. TextUtilities::Trim(result.question);
  1019. result.answers = options->toPollAnswers();
  1020. const auto solutionWithTags = quiz->checked()
  1021. ? solution->getTextWithAppliedMarkdown()
  1022. : TextWithTags();
  1023. result.solution = TextWithEntities{
  1024. solutionWithTags.text,
  1025. TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
  1026. };
  1027. const auto publicVotes = (anonymous && !anonymous->checked());
  1028. const auto multiChoice = (multiple && multiple->checked());
  1029. result.setFlags(Flag(0)
  1030. | (publicVotes ? Flag::PublicVotes : Flag(0))
  1031. | (multiChoice ? Flag::MultiChoice : Flag(0))
  1032. | (quiz->checked() ? Flag::Quiz : Flag(0)));
  1033. return result;
  1034. };
  1035. const auto collectError = [=] {
  1036. if (isValidQuestion()) {
  1037. *error &= ~Error::Question;
  1038. } else {
  1039. *error |= Error::Question;
  1040. }
  1041. if (!options->hasOptions()) {
  1042. *error |= Error::Options;
  1043. } else if (!options->isValid()) {
  1044. *error |= Error::Other;
  1045. } else {
  1046. *error &= ~(Error::Options | Error::Other);
  1047. }
  1048. if (quiz->checked() && !options->hasCorrect()) {
  1049. *error |= Error::Correct;
  1050. } else {
  1051. *error &= ~Error::Correct;
  1052. }
  1053. if (quiz->checked()
  1054. && solution->getLastText().trimmed().size() > kSolutionLimit) {
  1055. *error |= Error::Solution;
  1056. } else {
  1057. *error &= ~Error::Solution;
  1058. }
  1059. };
  1060. const auto showError = [show = uiShow()](
  1061. tr::phrase<> text) {
  1062. show->showToast(text(tr::now));
  1063. };
  1064. const auto send = [=](Api::SendOptions sendOptions) {
  1065. collectError();
  1066. if (*error & Error::Question) {
  1067. showError(tr::lng_polls_choose_question);
  1068. question->setFocus();
  1069. } else if (*error & Error::Options) {
  1070. showError(tr::lng_polls_choose_answers);
  1071. options->focusFirst();
  1072. } else if (*error & Error::Correct) {
  1073. showError(tr::lng_polls_choose_correct);
  1074. } else if (*error & Error::Solution) {
  1075. solution->showError();
  1076. } else if (!*error) {
  1077. _submitRequests.fire({ collectResult(), sendOptions });
  1078. }
  1079. };
  1080. const auto sendAction = SendMenu::DefaultCallback(
  1081. _controller->uiShow(),
  1082. crl::guard(this, send));
  1083. options->scrollToWidget(
  1084. ) | rpl::start_with_next([=](not_null<QWidget*> widget) {
  1085. scrollToWidget(widget);
  1086. }, lifetime());
  1087. options->backspaceInFront(
  1088. ) | rpl::start_with_next([=] {
  1089. FocusAtEnd(question);
  1090. }, lifetime());
  1091. const auto isNormal = (_sendType == Api::SendType::Normal);
  1092. const auto schedule = [=] {
  1093. sendAction(
  1094. { .type = SendMenu::ActionType::Schedule },
  1095. _sendMenuDetails());
  1096. };
  1097. const auto submit = addButton(
  1098. tr::lng_polls_create_button(),
  1099. [=] { isNormal ? send({}) : schedule(); });
  1100. submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
  1101. ? tr::lng_polls_create_button()
  1102. : tr::lng_schedule_button()));
  1103. const auto sendMenuDetails = [=] {
  1104. collectError();
  1105. return (*error) ? SendMenu::Details() : _sendMenuDetails();
  1106. };
  1107. SendMenu::SetupMenuAndShortcuts(
  1108. submit.data(),
  1109. _controller->uiShow(),
  1110. sendMenuDetails,
  1111. sendAction);
  1112. addButton(tr::lng_cancel(), [=] { closeBox(); });
  1113. return result;
  1114. }
  1115. void CreatePollBox::prepare() {
  1116. setTitle(tr::lng_polls_create_title());
  1117. const auto inner = setInnerWidget(setupContent());
  1118. setDimensionsToContent(st::boxWideWidth, inner);
  1119. }