tdesktop/Telegram/SourceFiles/support/support_autocomplete.cpp

591 lines
15 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 "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/input_fields.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 flags = MessageFlag::HasFromId
| MessageFlag::Outgoing
| MessageFlag::FakeHistoryItem;
const auto replyTo = MsgId();
const auto viaBotId = UserId();
const auto groupedId = uint64();
const auto item = history->makeMessage(
history->nextNonHistoryEntryId(),
flags,
replyTo,
viaBotId,
base::unixtime::now(),
history->session().userId(),
QString(),
TextWithEntities{ data.comment },
MTP_messageMediaEmpty(),
HistoryMessageMarkupData(),
groupedId);
return AdminLog::OwnedItem(delegate, item);
}
AdminLog::OwnedItem GenerateContactItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const Contact &data) {
const auto replyTo = MsgId();
const auto viaBotId = UserId();
const auto postAuthor = QString();
const auto groupedId = uint64();
const auto item = history->makeMessage(
history->nextNonHistoryEntryId(),
(MessageFlag::HasFromId
| MessageFlag::Outgoing
| MessageFlag::FakeHistoryItem),
replyTo,
viaBotId,
base::unixtime::now(),
history->session().userPeerId(),
postAuthor,
TextWithEntities(),
MTP_messageMediaContact(
MTP_string(data.phone),
MTP_string(data.firstName),
MTP_string(data.lastName),
MTP_string(), // vcard
MTP_long(0)), // user_id
HistoryMessageMarkupData(),
groupedId);
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());
connect(input, &Ui::InputField::blurred, [=] {
base::call_delayed(10, this, [=] {
if (!input->hasFocus()) {
deactivate();
}
});
});
connect(input, &Ui::InputField::cancelled, [=] { deactivate(); });
connect(input, &Ui::InputField::changed, refresh);
connect(input, &Ui::InputField::submitted, submit);
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>())
, _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