diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3288d109e..efa1d4ede 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1066,6 +1066,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_file" = "File"; "lng_in_dlg_sticker" = "Sticker"; "lng_in_dlg_sticker_emoji" = "{emoji} Sticker"; +"lng_in_dlg_poll" = "Poll"; "lng_ban_user" = "Ban User"; "lng_delete_all_from" = "Delete all from this user"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 418465494..7ec24cee1 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -863,6 +863,7 @@ createPollLimitLabel: FlatLabel(defaultFlatLabel) { minWidth: 274px; align: align(topleft); } +createPollLimitPadding: margins(22px, 10px, 22px, 5px); createPollOptionRemove: CrossButton { width: 22px; height: 22px; @@ -884,3 +885,10 @@ createPollOptionRemove: CrossButton { } } createPollOptionRemovePosition: point(10px, 7px); +createPollWarning: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + palette: TextPalette(defaultTextPalette) { + linkFg: boxTextFgError; + } +} +createPollWarningPosition: point(16px, 6px); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 64b3394aa..a16883ffb 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -15,10 +15,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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 { @@ -27,6 +29,7 @@ constexpr auto kMaxOptionsCount = 10; constexpr auto kOptionLimit = 100; constexpr auto kWarnQuestionLimit = 80; constexpr auto kWarnOptionLimit = 30; +constexpr auto kErrorLimit = 99; class Options { public: @@ -41,6 +44,7 @@ public: [[nodiscard]] rpl::producer<int> usedCount() const; [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; + [[nodiscard]] rpl::producer<> backspaceInFront() const; private: class Option { @@ -50,15 +54,14 @@ private: not_null<Ui::VerticalLayout*> container, int position); - [[nodisacrd]] bool hasShadow() const; - void createShadow(); - //void destroyShadow(); - - void createRemove(); void toggleRemoveAlways(bool toggled); + //[[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(); @@ -75,6 +78,10 @@ private: private: Option() = default; + void createShadow(); + void createRemove(); + void createWarning(); + base::unique_qptr<Ui::InputField> _field; base::unique_qptr<Ui::PlainShadow> _shadow; base::unique_qptr<Ui::CrossButton> _remove; @@ -99,6 +106,7 @@ private: rpl::variable<bool> _valid = false; rpl::variable<int> _usedCount = 0; rpl::event_stream<not_null<QWidget*>> _scrollToWidget; + rpl::event_stream<> _backspaceInFront; }; @@ -110,6 +118,38 @@ void InitField( 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, @@ -123,16 +163,18 @@ Options::Option Options::Option::Create( Ui::InputField::Mode::MultiLine, langFactory(lng_polls_create_option_add))); InitField(outer, field); + field->setMaxLength(kOptionLimit + kErrorLimit); result._field.reset(field); result.createShadow(); result.createRemove(); + result.createWarning(); return result; } -bool Options::Option::hasShadow() const { - return (_shadow != nullptr); -} +//bool Options::Option::hasShadow() const { +// return (_shadow != nullptr); +//} void Options::Option::createShadow() { Expects(_field != nullptr); @@ -195,13 +237,40 @@ void Options::Option::createRemove() { _remove.reset(remove); } +void Options::Option::createWarning() { + using namespace rpl::mappers; + + const auto field = _field.get(); + 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 { - const auto text = _field->getLastText().trimmed(); - return !text.isEmpty() && (text.size() <= kOptionLimit); + return !_field->getLastText().trimmed().isEmpty() && !isTooLong(); +} + +bool Options::Option::isTooLong() const { + return (_field->getLastText().size() > kOptionLimit); } bool Options::Option::hasFocus() const { @@ -209,7 +278,7 @@ bool Options::Option::hasFocus() const { } void Options::Option::setFocus() const { - _field->setFocus(); + FocusAtEnd(_field); } void Options::Option::clearValue() { @@ -272,6 +341,10 @@ rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const { return _scrollToWidget.events(); } +rpl::producer<> Options::backspaceInFront() const { + return _backspaceInFront.events(); +} + std::vector<PollAnswer> Options::toPollAnswers() const { auto result = std::vector<PollAnswer>(); result.reserve(_list.size()); @@ -378,6 +451,24 @@ void Options::addEmptyOption() { 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([=] { @@ -403,7 +494,8 @@ void Options::addEmptyOption() { void Options::validateState() { checkLastOption(); - _valid = (ranges::count_if(_list, &Option::isGood) > 1); + _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); } @@ -454,6 +546,29 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion( 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; } @@ -486,7 +601,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() { container, std::move(limit), st::createPollLimitLabel), - st::createPollFieldPadding); + st::createPollLimitPadding); const auto isValidQuestion = [=] { const auto text = question->getLastText().trimmed(); @@ -512,7 +627,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() { const auto updateValid = [=] { valid->fire(isValidQuestion() && options->isValid()); }; - valid->events( + connect(question, &Ui::InputField::changed, [=] { + updateValid(); + }); + valid->events_starting_with( + false ) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool valid) { clearButtons(); @@ -534,6 +653,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() { scrollToWidget(widget); }, lifetime()); + options->backspaceInFront( + ) | rpl::start_with_next([=] { + FocusAtEnd(question); + }, lifetime()); + return std::move(result); } @@ -543,4 +667,7 @@ void CreatePollBox::prepare() { const auto inner = setInnerWidget(setupContent()); setDimensionsToContent(st::boxWideWidth, inner); + + setCloseByEscape(false); + setCloseByOutsideClick(false); } diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 5f7f5dfff..961ffd448 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1216,12 +1216,8 @@ PollData *MediaPoll::poll() const { return _poll; } -QString MediaPoll::chatsListText() const { - return QString(); // #TODO polls -} - QString MediaPoll::notificationText() const { - return QString(); // #TODO polls + return lang(lng_in_dlg_poll); } QString MediaPoll::pinnedTextSubstring() const { @@ -1229,7 +1225,19 @@ QString MediaPoll::pinnedTextSubstring() const { } TextWithEntities MediaPoll::clipboardText() const { - return TextWithEntities(); // #TODO polls + const auto text = qsl("[ ") + + lang(lng_in_dlg_poll) + + qsl(" : ") + + _poll->question + + qsl(" ]") + + ranges::accumulate( + ranges::view::all( + _poll->answers + ) | ranges::view::transform( + [](const PollAnswer &answer) { return "\n- " + answer.text; } + ), + QString()); + return { text, EntitiesInText() }; } bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) { diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index c6b1e2928..1110578b0 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -390,7 +390,6 @@ public: PollData *poll() const override; - QString chatsListText() const override; QString notificationText() const override; QString pinnedTextSubstring() const override; TextWithEntities clipboardText() const override;