tdesktop/Telegram/SourceFiles/boxes/create_poll_box.cpp
John Preston 7b5e5c2587 Move caption to the next album item on cancel.
After #4869 albums are sent with captions in the first media.
In case we cancel the first media leaving the rest of the album
the caption will be lost unless we move it to the new "first" media.
2018-12-26 11:24:12 +04:00

729 lines
18 KiB
C++

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