| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- /*
- 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 "support/support_autocomplete.h"
- #include "ui/chat/chat_theme.h"
- #include "ui/chat/chat_style.h"
- #include "ui/widgets/scroll_area.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/widgets/buttons.h"
- #include "ui/wrap/padding_wrap.h"
- #include "ui/painter.h"
- #include "support/support_templates.h"
- #include "support/support_common.h"
- #include "history/view/history_view_message.h"
- #include "history/view/history_view_service_message.h"
- #include "history/history_item.h"
- #include "lang/lang_keys.h"
- #include "base/unixtime.h"
- #include "base/call_delayed.h"
- #include "main/main_session.h"
- #include "main/main_session_settings.h"
- #include "apiwrap.h"
- #include "window/window_session_controller.h"
- #include "styles/style_chat_helpers.h"
- #include "styles/style_window.h"
- #include "styles/style_layers.h"
- namespace Support {
- namespace {
- class Inner : public Ui::RpWidget {
- public:
- Inner(QWidget *parent);
- using Question = details::TemplatesQuestion;
- void showRows(std::vector<Question> &&rows);
- std::pair<int, int> moveSelection(int delta);
- std::optional<Question> selected() const;
- auto activated() const {
- return _activated.events();
- }
- protected:
- void paintEvent(QPaintEvent *e) override;
- void mouseMoveEvent(QMouseEvent *e) override;
- void mousePressEvent(QMouseEvent *e) override;
- void mouseReleaseEvent(QMouseEvent *e) override;
- void leaveEventHook(QEvent *e) override;
- int resizeGetHeight(int newWidth) override;
- private:
- struct Row {
- Question data;
- Ui::Text::String question = { st::windowMinWidth / 2 };
- Ui::Text::String keys = { st::windowMinWidth / 2 };
- Ui::Text::String answer = { st::windowMinWidth / 2 };
- int top = 0;
- int height = 0;
- };
- void prepareRow(Row &row);
- int resizeRowGetHeight(Row &row, int newWidth);
- void setSelected(int selected);
- std::vector<Row> _rows;
- int _selected = -1;
- int _pressed = -1;
- bool _selectByKeys = false;
- rpl::event_stream<> _activated;
- };
- int TextHeight(const Ui::Text::String &text, int available, int lines) {
- Expects(text.style() != nullptr);
- const auto st = text.style();
- const auto line = st->lineHeight ? st->lineHeight : st->font->height;
- return std::min(text.countHeight(available), lines * line);
- };
- Inner::Inner(QWidget *parent) : RpWidget(parent) {
- setMouseTracking(true);
- }
- void Inner::showRows(std::vector<Question> &&rows) {
- _rows.resize(0);
- _rows.reserve(rows.size());
- for (auto &row : rows) {
- _rows.push_back({ std::move(row) });
- auto &added = _rows.back();
- prepareRow(added);
- }
- resizeToWidth(width());
- _selected = _pressed = -1;
- moveSelection(1);
- update();
- }
- std::pair<int, int> Inner::moveSelection(int delta) {
- const auto selected = _selected + delta;
- if (selected >= 0 && selected < _rows.size()) {
- _selectByKeys = true;
- setSelected(selected);
- const auto top = _rows[_selected].top;
- return { top, top + _rows[_selected].height };
- }
- return { -1, -1 };
- }
- auto Inner::selected() const -> std::optional<Question> {
- if (_rows.empty()) {
- return std::nullopt;
- } else if (_selected < 0) {
- return _rows[0].data;
- }
- return _rows[_selected].data;
- }
- void Inner::prepareRow(Row &row) {
- row.question.setText(st::autocompleteRowTitle, row.data.question);
- row.keys.setText(
- st::autocompleteRowKeys,
- row.data.originalKeys.join(u", "_q));
- row.answer.setText(st::autocompleteRowAnswer, row.data.value);
- }
- int Inner::resizeRowGetHeight(Row &row, int newWidth) {
- const auto available = newWidth
- - st::autocompleteRowPadding.left()
- - st::autocompleteRowPadding.right();
- return row.height = st::autocompleteRowPadding.top()
- + TextHeight(row.question, available, 1)
- + TextHeight(row.keys, available, 1)
- + TextHeight(row.answer, available, 2)
- + st::autocompleteRowPadding.bottom()
- + st::lineWidth;
- }
- int Inner::resizeGetHeight(int newWidth) {
- auto top = 0;
- for (auto &row : _rows) {
- row.top = top;
- top += resizeRowGetHeight(row, newWidth);
- }
- return top ? (top - st::lineWidth) : (3 * st::mentionHeight);
- }
- void Inner::paintEvent(QPaintEvent *e) {
- Painter p(this);
- if (_rows.empty()) {
- p.setFont(st::boxTextFont);
- p.setPen(st::windowSubTextFg);
- p.drawText(
- rect(),
- "Search by question, keys or value",
- style::al_center);
- return;
- }
- const auto clip = e->rect();
- const auto from = ranges::upper_bound(
- _rows,
- clip.y(),
- std::less<>(),
- [](const Row &row) { return row.top + row.height; });
- const auto till = ranges::lower_bound(
- _rows,
- clip.y() + clip.height(),
- std::less<>(),
- [](const Row &row) { return row.top; });
- if (from == end(_rows)) {
- return;
- }
- p.translate(0, from->top);
- const auto padding = st::autocompleteRowPadding;
- const auto available = width() - padding.left() - padding.right();
- auto top = padding.top();
- const auto drawText = [&](const Ui::Text::String &text, int lines) {
- text.drawLeftElided(
- p,
- padding.left(),
- top,
- available,
- width(),
- lines);
- top += TextHeight(text, available, lines);
- };
- for (auto i = from; i != till; ++i) {
- const auto over = (i - begin(_rows) == _selected);
- if (over) {
- p.fillRect(0, 0, width(), i->height, st::windowBgOver);
- }
- p.setPen(st::mentionNameFg);
- drawText(i->question, 1);
- p.setPen(over ? st::mentionFgOver : st::mentionFg);
- drawText(i->keys, 1);
- p.setPen(st::windowFg);
- drawText(i->answer, 2);
- p.translate(0, i->height);
- top = padding.top();
- if (i - begin(_rows) + 1 == _selected) {
- p.fillRect(
- 0,
- -st::lineWidth,
- width(),
- st::lineWidth,
- st::windowBgOver);
- } else if (!over) {
- p.fillRect(
- padding.left(),
- -st::lineWidth,
- available,
- st::lineWidth,
- st::shadowFg);
- }
- }
- }
- void Inner::mouseMoveEvent(QMouseEvent *e) {
- static auto lastGlobalPos = QPoint();
- const auto moved = (e->globalPos() != lastGlobalPos);
- if (!moved && _selectByKeys) {
- return;
- }
- _selectByKeys = false;
- lastGlobalPos = e->globalPos();
- const auto i = ranges::upper_bound(
- _rows,
- e->pos().y(),
- std::less<>(),
- [](const Row &row) { return row.top + row.height; });
- setSelected((i == end(_rows)) ? -1 : (i - begin(_rows)));
- }
- void Inner::leaveEventHook(QEvent *e) {
- setSelected(-1);
- }
- void Inner::setSelected(int selected) {
- if (_selected != selected) {
- _selected = selected;
- update();
- }
- }
- void Inner::mousePressEvent(QMouseEvent *e) {
- _pressed = _selected;
- }
- void Inner::mouseReleaseEvent(QMouseEvent *e) {
- const auto pressed = base::take(_pressed);
- if (pressed == _selected && pressed >= 0) {
- _activated.fire({});
- }
- }
- AdminLog::OwnedItem GenerateCommentItem(
- not_null<HistoryView::ElementDelegate*> delegate,
- not_null<History*> history,
- const Contact &data) {
- if (data.comment.isEmpty()) {
- return nullptr;
- }
- const auto item = history->makeMessage({
- .id = history->nextNonHistoryEntryId(),
- .flags = (MessageFlag::HasFromId
- | MessageFlag::Outgoing
- | MessageFlag::FakeHistoryItem),
- .from = history->session().userPeerId(),
- .date = base::unixtime::now(),
- }, TextWithEntities{ data.comment }, MTP_messageMediaEmpty());
- return AdminLog::OwnedItem(delegate, item);
- }
- AdminLog::OwnedItem GenerateContactItem(
- not_null<HistoryView::ElementDelegate*> delegate,
- not_null<History*> history,
- const Contact &data) {
- const auto item = history->makeMessage({
- .id = history->nextNonHistoryEntryId(),
- .flags = (MessageFlag::HasFromId
- | MessageFlag::Outgoing
- | MessageFlag::FakeHistoryItem),
- .from = history->session().userPeerId(),
- .date = base::unixtime::now(),
- }, TextWithEntities(), MTP_messageMediaContact(
- MTP_string(data.phone),
- MTP_string(data.firstName),
- MTP_string(data.lastName),
- MTP_string(), // vcard
- MTP_long(0))); // user_id
- return AdminLog::OwnedItem(delegate, item);
- }
- } // namespace
- Autocomplete::Autocomplete(QWidget *parent, not_null<Main::Session*> session)
- : RpWidget(parent)
- , _session(session) {
- setupContent();
- }
- void Autocomplete::activate(not_null<Ui::InputField*> field) {
- if (_session->settings().supportTemplatesAutocomplete()) {
- _activate();
- } else {
- const auto &templates = _session->supportTemplates();
- const auto max = templates.maxKeyLength();
- auto cursor = field->textCursor();
- const auto position = cursor.position();
- const auto anchor = cursor.anchor();
- const auto text = (position != anchor)
- ? field->getTextWithTagsPart(
- std::min(position, anchor),
- std::max(position, anchor))
- : field->getTextWithTagsPart(
- std::max(position - max, 0),
- position);
- const auto result = (position != anchor)
- ? templates.matchExact(text.text)
- : templates.matchFromEnd(text.text);
- if (result) {
- const auto till = std::max(position, anchor);
- const auto from = till - result->key.size();
- cursor.setPosition(from);
- cursor.setPosition(till, QTextCursor::KeepAnchor);
- field->setTextCursor(cursor);
- submitValue(result->question.value);
- }
- }
- }
- void Autocomplete::deactivate() {
- _deactivate();
- }
- void Autocomplete::setBoundings(QRect rect) {
- const auto maxHeight = int(4.5 * st::mentionHeight);
- const auto height = std::min(rect.height(), maxHeight);
- setGeometry(
- rect.x(),
- rect.y() + rect.height() - height,
- rect.width(),
- height);
- }
- rpl::producer<QString> Autocomplete::insertRequests() const {
- return _insertRequests.events();
- }
- rpl::producer<Contact> Autocomplete::shareContactRequests() const {
- return _shareContactRequests.events();
- }
- void Autocomplete::keyPressEvent(QKeyEvent *e) {
- if (e->key() == Qt::Key_Up) {
- _moveSelection(-1);
- } else if (e->key() == Qt::Key_Down) {
- _moveSelection(1);
- }
- }
- void Autocomplete::setupContent() {
- const auto inputWrap = Ui::CreateChild<Ui::PaddingWrap<Ui::InputField>>(
- this,
- object_ptr<Ui::InputField>(
- this,
- st::defaultMultiSelectSearchField,
- rpl::single(u"Search for templates"_q)), // #TODO hard_lang
- st::autocompleteSearchPadding);
- const auto input = inputWrap->entity();
- const auto scroll = Ui::CreateChild<Ui::ScrollArea>(this);
- const auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));
- const auto submit = [=] {
- if (const auto question = inner->selected()) {
- submitValue(question->value);
- }
- };
- const auto refresh = [=] {
- inner->showRows(
- _session->supportTemplates().query(input->getLastText()));
- scroll->scrollToY(0);
- };
- inner->activated() | rpl::start_with_next(submit, lifetime());
- input->focusedChanges(
- ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] {
- base::call_delayed(10, this, [=] {
- if (!input->hasFocus()) {
- deactivate();
- }
- });
- }, input->lifetime());
- input->cancelled(
- ) | rpl::start_with_next([=] {
- deactivate();
- }, input->lifetime());
- input->changes() | rpl::start_with_next(refresh, input->lifetime());
- input->submits() | rpl::start_with_next(submit, input->lifetime());
- input->customUpDown(true);
- _activate = [=] {
- input->setText(QString());
- show();
- input->setFocus();
- };
- _deactivate = [=] {
- hide();
- };
- _moveSelection = [=](int delta) {
- const auto range = inner->moveSelection(delta);
- if (range.second > range.first) {
- scroll->scrollToY(range.first, range.second);
- }
- };
- paintRequest(
- ) | rpl::start_with_next([=](QRect clip) {
- QPainter p(this);
- p.fillRect(
- clip.intersected(QRect(0, st::lineWidth, width(), height())),
- st::mentionBg);
- p.fillRect(
- clip.intersected(QRect(0, 0, width(), st::lineWidth)),
- st::shadowFg);
- }, lifetime());
- sizeValue(
- ) | rpl::start_with_next([=](QSize size) {
- inputWrap->resizeToWidth(size.width());
- inputWrap->moveToLeft(0, st::lineWidth, size.width());
- scroll->setGeometry(
- 0,
- inputWrap->height(),
- size.width(),
- size.height() - inputWrap->height() - st::lineWidth);
- inner->resizeToWidth(size.width());
- }, lifetime());
- }
- void Autocomplete::submitValue(const QString &value) {
- const auto prefix = u"contact:"_q;
- if (value.startsWith(prefix)) {
- const auto line = value.indexOf('\n');
- const auto text = (line > 0) ? value.mid(line + 1) : QString();
- const auto contact = value.mid(
- prefix.size(),
- (line > 0) ? (line - prefix.size()) : -1);
- const auto parts = contact.split(' ', Qt::SkipEmptyParts);
- if (parts.size() > 1) {
- const auto phone = parts[0];
- const auto firstName = parts[1];
- const auto lastName = (parts.size() > 2)
- ? QStringList(parts.mid(2)).join(' ')
- : QString();
- _shareContactRequests.fire(Contact{
- text,
- phone,
- firstName,
- lastName });
- }
- } else {
- _insertRequests.fire_copy(value);
- }
- }
- ConfirmContactBox::ConfirmContactBox(
- QWidget*,
- not_null<Window::SessionController*> controller,
- not_null<History*> history,
- const Contact &data,
- Fn<void(Qt::KeyboardModifiers)> submit)
- : SimpleElementDelegate(controller, [=] { update(); })
- , _chatStyle(std::make_unique<Ui::ChatStyle>(
- history->session().colorIndicesValue()))
- , _comment(GenerateCommentItem(this, history, data))
- , _contact(GenerateContactItem(this, history, data))
- , _submit(submit) {
- _chatStyle->apply(controller->defaultChatTheme().get());
- }
- void ConfirmContactBox::prepare() {
- setTitle(rpl::single(u"Confirmation"_q)); // #TODO hard_lang
- auto maxWidth = 0;
- if (_comment) {
- _comment->setAttachToNext(true, _contact.get());
- _contact->setAttachToPrevious(true, _comment.get());
- _comment->initDimensions();
- accumulate_max(maxWidth, _comment->maxWidth());
- }
- _contact->initDimensions();
- accumulate_max(maxWidth, _contact->maxWidth());
- maxWidth += st::boxPadding.left() + st::boxPadding.right();
- const auto width = std::clamp(maxWidth, st::boxWidth, st::boxWideWidth);
- const auto available = width
- - st::boxPadding.left()
- - st::boxPadding.right();
- auto height = 0;
- if (_comment) {
- height += _comment->resizeGetHeight(available);
- }
- height += _contact->resizeGetHeight(available);
- setDimensions(width, height);
- _contact->initDimensions();
- _submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) {
- const auto weak = Ui::MakeWeak(this);
- original(m);
- if (weak) {
- closeBox();
- }
- };
- const auto button = addButton(tr::lng_send_button(), [] {});
- button->clicks(
- ) | rpl::start_with_next([=](Qt::MouseButton which) {
- _submit((which == Qt::RightButton)
- ? SkipSwitchModifiers()
- : button->clickModifiers());
- }, button->lifetime());
- button->setAcceptBoth(true);
- addButton(tr::lng_cancel(), [=] { closeBox(); });
- }
- void ConfirmContactBox::keyPressEvent(QKeyEvent *e) {
- if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
- _submit(e->modifiers());
- } else {
- BoxContent::keyPressEvent(e);
- }
- }
- void ConfirmContactBox::paintEvent(QPaintEvent *e) {
- Painter p(this);
- p.fillRect(e->rect(), st::boxBg);
- const auto theme = controller()->defaultChatTheme().get();
- auto context = theme->preparePaintContext(
- _chatStyle.get(),
- rect(),
- rect(),
- controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));
- p.translate(st::boxPadding.left(), 0);
- if (_comment) {
- context.outbg = _comment->hasOutLayout();
- _comment->draw(p, context);
- p.translate(0, _comment->height());
- }
- context.outbg = _contact->hasOutLayout();
- _contact->draw(p, context);
- }
- HistoryView::Context ConfirmContactBox::elementContext() {
- return HistoryView::Context::ContactPreview;
- }
- } // namespace Support
|