support_autocomplete.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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 "support/support_autocomplete.h"
  8. #include "ui/chat/chat_theme.h"
  9. #include "ui/chat/chat_style.h"
  10. #include "ui/widgets/scroll_area.h"
  11. #include "ui/widgets/fields/input_field.h"
  12. #include "ui/widgets/buttons.h"
  13. #include "ui/wrap/padding_wrap.h"
  14. #include "ui/painter.h"
  15. #include "support/support_templates.h"
  16. #include "support/support_common.h"
  17. #include "history/view/history_view_message.h"
  18. #include "history/view/history_view_service_message.h"
  19. #include "history/history_item.h"
  20. #include "lang/lang_keys.h"
  21. #include "base/unixtime.h"
  22. #include "base/call_delayed.h"
  23. #include "main/main_session.h"
  24. #include "main/main_session_settings.h"
  25. #include "apiwrap.h"
  26. #include "window/window_session_controller.h"
  27. #include "styles/style_chat_helpers.h"
  28. #include "styles/style_window.h"
  29. #include "styles/style_layers.h"
  30. namespace Support {
  31. namespace {
  32. class Inner : public Ui::RpWidget {
  33. public:
  34. Inner(QWidget *parent);
  35. using Question = details::TemplatesQuestion;
  36. void showRows(std::vector<Question> &&rows);
  37. std::pair<int, int> moveSelection(int delta);
  38. std::optional<Question> selected() const;
  39. auto activated() const {
  40. return _activated.events();
  41. }
  42. protected:
  43. void paintEvent(QPaintEvent *e) override;
  44. void mouseMoveEvent(QMouseEvent *e) override;
  45. void mousePressEvent(QMouseEvent *e) override;
  46. void mouseReleaseEvent(QMouseEvent *e) override;
  47. void leaveEventHook(QEvent *e) override;
  48. int resizeGetHeight(int newWidth) override;
  49. private:
  50. struct Row {
  51. Question data;
  52. Ui::Text::String question = { st::windowMinWidth / 2 };
  53. Ui::Text::String keys = { st::windowMinWidth / 2 };
  54. Ui::Text::String answer = { st::windowMinWidth / 2 };
  55. int top = 0;
  56. int height = 0;
  57. };
  58. void prepareRow(Row &row);
  59. int resizeRowGetHeight(Row &row, int newWidth);
  60. void setSelected(int selected);
  61. std::vector<Row> _rows;
  62. int _selected = -1;
  63. int _pressed = -1;
  64. bool _selectByKeys = false;
  65. rpl::event_stream<> _activated;
  66. };
  67. int TextHeight(const Ui::Text::String &text, int available, int lines) {
  68. Expects(text.style() != nullptr);
  69. const auto st = text.style();
  70. const auto line = st->lineHeight ? st->lineHeight : st->font->height;
  71. return std::min(text.countHeight(available), lines * line);
  72. };
  73. Inner::Inner(QWidget *parent) : RpWidget(parent) {
  74. setMouseTracking(true);
  75. }
  76. void Inner::showRows(std::vector<Question> &&rows) {
  77. _rows.resize(0);
  78. _rows.reserve(rows.size());
  79. for (auto &row : rows) {
  80. _rows.push_back({ std::move(row) });
  81. auto &added = _rows.back();
  82. prepareRow(added);
  83. }
  84. resizeToWidth(width());
  85. _selected = _pressed = -1;
  86. moveSelection(1);
  87. update();
  88. }
  89. std::pair<int, int> Inner::moveSelection(int delta) {
  90. const auto selected = _selected + delta;
  91. if (selected >= 0 && selected < _rows.size()) {
  92. _selectByKeys = true;
  93. setSelected(selected);
  94. const auto top = _rows[_selected].top;
  95. return { top, top + _rows[_selected].height };
  96. }
  97. return { -1, -1 };
  98. }
  99. auto Inner::selected() const -> std::optional<Question> {
  100. if (_rows.empty()) {
  101. return std::nullopt;
  102. } else if (_selected < 0) {
  103. return _rows[0].data;
  104. }
  105. return _rows[_selected].data;
  106. }
  107. void Inner::prepareRow(Row &row) {
  108. row.question.setText(st::autocompleteRowTitle, row.data.question);
  109. row.keys.setText(
  110. st::autocompleteRowKeys,
  111. row.data.originalKeys.join(u", "_q));
  112. row.answer.setText(st::autocompleteRowAnswer, row.data.value);
  113. }
  114. int Inner::resizeRowGetHeight(Row &row, int newWidth) {
  115. const auto available = newWidth
  116. - st::autocompleteRowPadding.left()
  117. - st::autocompleteRowPadding.right();
  118. return row.height = st::autocompleteRowPadding.top()
  119. + TextHeight(row.question, available, 1)
  120. + TextHeight(row.keys, available, 1)
  121. + TextHeight(row.answer, available, 2)
  122. + st::autocompleteRowPadding.bottom()
  123. + st::lineWidth;
  124. }
  125. int Inner::resizeGetHeight(int newWidth) {
  126. auto top = 0;
  127. for (auto &row : _rows) {
  128. row.top = top;
  129. top += resizeRowGetHeight(row, newWidth);
  130. }
  131. return top ? (top - st::lineWidth) : (3 * st::mentionHeight);
  132. }
  133. void Inner::paintEvent(QPaintEvent *e) {
  134. Painter p(this);
  135. if (_rows.empty()) {
  136. p.setFont(st::boxTextFont);
  137. p.setPen(st::windowSubTextFg);
  138. p.drawText(
  139. rect(),
  140. "Search by question, keys or value",
  141. style::al_center);
  142. return;
  143. }
  144. const auto clip = e->rect();
  145. const auto from = ranges::upper_bound(
  146. _rows,
  147. clip.y(),
  148. std::less<>(),
  149. [](const Row &row) { return row.top + row.height; });
  150. const auto till = ranges::lower_bound(
  151. _rows,
  152. clip.y() + clip.height(),
  153. std::less<>(),
  154. [](const Row &row) { return row.top; });
  155. if (from == end(_rows)) {
  156. return;
  157. }
  158. p.translate(0, from->top);
  159. const auto padding = st::autocompleteRowPadding;
  160. const auto available = width() - padding.left() - padding.right();
  161. auto top = padding.top();
  162. const auto drawText = [&](const Ui::Text::String &text, int lines) {
  163. text.drawLeftElided(
  164. p,
  165. padding.left(),
  166. top,
  167. available,
  168. width(),
  169. lines);
  170. top += TextHeight(text, available, lines);
  171. };
  172. for (auto i = from; i != till; ++i) {
  173. const auto over = (i - begin(_rows) == _selected);
  174. if (over) {
  175. p.fillRect(0, 0, width(), i->height, st::windowBgOver);
  176. }
  177. p.setPen(st::mentionNameFg);
  178. drawText(i->question, 1);
  179. p.setPen(over ? st::mentionFgOver : st::mentionFg);
  180. drawText(i->keys, 1);
  181. p.setPen(st::windowFg);
  182. drawText(i->answer, 2);
  183. p.translate(0, i->height);
  184. top = padding.top();
  185. if (i - begin(_rows) + 1 == _selected) {
  186. p.fillRect(
  187. 0,
  188. -st::lineWidth,
  189. width(),
  190. st::lineWidth,
  191. st::windowBgOver);
  192. } else if (!over) {
  193. p.fillRect(
  194. padding.left(),
  195. -st::lineWidth,
  196. available,
  197. st::lineWidth,
  198. st::shadowFg);
  199. }
  200. }
  201. }
  202. void Inner::mouseMoveEvent(QMouseEvent *e) {
  203. static auto lastGlobalPos = QPoint();
  204. const auto moved = (e->globalPos() != lastGlobalPos);
  205. if (!moved && _selectByKeys) {
  206. return;
  207. }
  208. _selectByKeys = false;
  209. lastGlobalPos = e->globalPos();
  210. const auto i = ranges::upper_bound(
  211. _rows,
  212. e->pos().y(),
  213. std::less<>(),
  214. [](const Row &row) { return row.top + row.height; });
  215. setSelected((i == end(_rows)) ? -1 : (i - begin(_rows)));
  216. }
  217. void Inner::leaveEventHook(QEvent *e) {
  218. setSelected(-1);
  219. }
  220. void Inner::setSelected(int selected) {
  221. if (_selected != selected) {
  222. _selected = selected;
  223. update();
  224. }
  225. }
  226. void Inner::mousePressEvent(QMouseEvent *e) {
  227. _pressed = _selected;
  228. }
  229. void Inner::mouseReleaseEvent(QMouseEvent *e) {
  230. const auto pressed = base::take(_pressed);
  231. if (pressed == _selected && pressed >= 0) {
  232. _activated.fire({});
  233. }
  234. }
  235. AdminLog::OwnedItem GenerateCommentItem(
  236. not_null<HistoryView::ElementDelegate*> delegate,
  237. not_null<History*> history,
  238. const Contact &data) {
  239. if (data.comment.isEmpty()) {
  240. return nullptr;
  241. }
  242. const auto item = history->makeMessage({
  243. .id = history->nextNonHistoryEntryId(),
  244. .flags = (MessageFlag::HasFromId
  245. | MessageFlag::Outgoing
  246. | MessageFlag::FakeHistoryItem),
  247. .from = history->session().userPeerId(),
  248. .date = base::unixtime::now(),
  249. }, TextWithEntities{ data.comment }, MTP_messageMediaEmpty());
  250. return AdminLog::OwnedItem(delegate, item);
  251. }
  252. AdminLog::OwnedItem GenerateContactItem(
  253. not_null<HistoryView::ElementDelegate*> delegate,
  254. not_null<History*> history,
  255. const Contact &data) {
  256. const auto item = history->makeMessage({
  257. .id = history->nextNonHistoryEntryId(),
  258. .flags = (MessageFlag::HasFromId
  259. | MessageFlag::Outgoing
  260. | MessageFlag::FakeHistoryItem),
  261. .from = history->session().userPeerId(),
  262. .date = base::unixtime::now(),
  263. }, TextWithEntities(), MTP_messageMediaContact(
  264. MTP_string(data.phone),
  265. MTP_string(data.firstName),
  266. MTP_string(data.lastName),
  267. MTP_string(), // vcard
  268. MTP_long(0))); // user_id
  269. return AdminLog::OwnedItem(delegate, item);
  270. }
  271. } // namespace
  272. Autocomplete::Autocomplete(QWidget *parent, not_null<Main::Session*> session)
  273. : RpWidget(parent)
  274. , _session(session) {
  275. setupContent();
  276. }
  277. void Autocomplete::activate(not_null<Ui::InputField*> field) {
  278. if (_session->settings().supportTemplatesAutocomplete()) {
  279. _activate();
  280. } else {
  281. const auto &templates = _session->supportTemplates();
  282. const auto max = templates.maxKeyLength();
  283. auto cursor = field->textCursor();
  284. const auto position = cursor.position();
  285. const auto anchor = cursor.anchor();
  286. const auto text = (position != anchor)
  287. ? field->getTextWithTagsPart(
  288. std::min(position, anchor),
  289. std::max(position, anchor))
  290. : field->getTextWithTagsPart(
  291. std::max(position - max, 0),
  292. position);
  293. const auto result = (position != anchor)
  294. ? templates.matchExact(text.text)
  295. : templates.matchFromEnd(text.text);
  296. if (result) {
  297. const auto till = std::max(position, anchor);
  298. const auto from = till - result->key.size();
  299. cursor.setPosition(from);
  300. cursor.setPosition(till, QTextCursor::KeepAnchor);
  301. field->setTextCursor(cursor);
  302. submitValue(result->question.value);
  303. }
  304. }
  305. }
  306. void Autocomplete::deactivate() {
  307. _deactivate();
  308. }
  309. void Autocomplete::setBoundings(QRect rect) {
  310. const auto maxHeight = int(4.5 * st::mentionHeight);
  311. const auto height = std::min(rect.height(), maxHeight);
  312. setGeometry(
  313. rect.x(),
  314. rect.y() + rect.height() - height,
  315. rect.width(),
  316. height);
  317. }
  318. rpl::producer<QString> Autocomplete::insertRequests() const {
  319. return _insertRequests.events();
  320. }
  321. rpl::producer<Contact> Autocomplete::shareContactRequests() const {
  322. return _shareContactRequests.events();
  323. }
  324. void Autocomplete::keyPressEvent(QKeyEvent *e) {
  325. if (e->key() == Qt::Key_Up) {
  326. _moveSelection(-1);
  327. } else if (e->key() == Qt::Key_Down) {
  328. _moveSelection(1);
  329. }
  330. }
  331. void Autocomplete::setupContent() {
  332. const auto inputWrap = Ui::CreateChild<Ui::PaddingWrap<Ui::InputField>>(
  333. this,
  334. object_ptr<Ui::InputField>(
  335. this,
  336. st::defaultMultiSelectSearchField,
  337. rpl::single(u"Search for templates"_q)), // #TODO hard_lang
  338. st::autocompleteSearchPadding);
  339. const auto input = inputWrap->entity();
  340. const auto scroll = Ui::CreateChild<Ui::ScrollArea>(this);
  341. const auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));
  342. const auto submit = [=] {
  343. if (const auto question = inner->selected()) {
  344. submitValue(question->value);
  345. }
  346. };
  347. const auto refresh = [=] {
  348. inner->showRows(
  349. _session->supportTemplates().query(input->getLastText()));
  350. scroll->scrollToY(0);
  351. };
  352. inner->activated() | rpl::start_with_next(submit, lifetime());
  353. input->focusedChanges(
  354. ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] {
  355. base::call_delayed(10, this, [=] {
  356. if (!input->hasFocus()) {
  357. deactivate();
  358. }
  359. });
  360. }, input->lifetime());
  361. input->cancelled(
  362. ) | rpl::start_with_next([=] {
  363. deactivate();
  364. }, input->lifetime());
  365. input->changes() | rpl::start_with_next(refresh, input->lifetime());
  366. input->submits() | rpl::start_with_next(submit, input->lifetime());
  367. input->customUpDown(true);
  368. _activate = [=] {
  369. input->setText(QString());
  370. show();
  371. input->setFocus();
  372. };
  373. _deactivate = [=] {
  374. hide();
  375. };
  376. _moveSelection = [=](int delta) {
  377. const auto range = inner->moveSelection(delta);
  378. if (range.second > range.first) {
  379. scroll->scrollToY(range.first, range.second);
  380. }
  381. };
  382. paintRequest(
  383. ) | rpl::start_with_next([=](QRect clip) {
  384. QPainter p(this);
  385. p.fillRect(
  386. clip.intersected(QRect(0, st::lineWidth, width(), height())),
  387. st::mentionBg);
  388. p.fillRect(
  389. clip.intersected(QRect(0, 0, width(), st::lineWidth)),
  390. st::shadowFg);
  391. }, lifetime());
  392. sizeValue(
  393. ) | rpl::start_with_next([=](QSize size) {
  394. inputWrap->resizeToWidth(size.width());
  395. inputWrap->moveToLeft(0, st::lineWidth, size.width());
  396. scroll->setGeometry(
  397. 0,
  398. inputWrap->height(),
  399. size.width(),
  400. size.height() - inputWrap->height() - st::lineWidth);
  401. inner->resizeToWidth(size.width());
  402. }, lifetime());
  403. }
  404. void Autocomplete::submitValue(const QString &value) {
  405. const auto prefix = u"contact:"_q;
  406. if (value.startsWith(prefix)) {
  407. const auto line = value.indexOf('\n');
  408. const auto text = (line > 0) ? value.mid(line + 1) : QString();
  409. const auto contact = value.mid(
  410. prefix.size(),
  411. (line > 0) ? (line - prefix.size()) : -1);
  412. const auto parts = contact.split(' ', Qt::SkipEmptyParts);
  413. if (parts.size() > 1) {
  414. const auto phone = parts[0];
  415. const auto firstName = parts[1];
  416. const auto lastName = (parts.size() > 2)
  417. ? QStringList(parts.mid(2)).join(' ')
  418. : QString();
  419. _shareContactRequests.fire(Contact{
  420. text,
  421. phone,
  422. firstName,
  423. lastName });
  424. }
  425. } else {
  426. _insertRequests.fire_copy(value);
  427. }
  428. }
  429. ConfirmContactBox::ConfirmContactBox(
  430. QWidget*,
  431. not_null<Window::SessionController*> controller,
  432. not_null<History*> history,
  433. const Contact &data,
  434. Fn<void(Qt::KeyboardModifiers)> submit)
  435. : SimpleElementDelegate(controller, [=] { update(); })
  436. , _chatStyle(std::make_unique<Ui::ChatStyle>(
  437. history->session().colorIndicesValue()))
  438. , _comment(GenerateCommentItem(this, history, data))
  439. , _contact(GenerateContactItem(this, history, data))
  440. , _submit(submit) {
  441. _chatStyle->apply(controller->defaultChatTheme().get());
  442. }
  443. void ConfirmContactBox::prepare() {
  444. setTitle(rpl::single(u"Confirmation"_q)); // #TODO hard_lang
  445. auto maxWidth = 0;
  446. if (_comment) {
  447. _comment->setAttachToNext(true, _contact.get());
  448. _contact->setAttachToPrevious(true, _comment.get());
  449. _comment->initDimensions();
  450. accumulate_max(maxWidth, _comment->maxWidth());
  451. }
  452. _contact->initDimensions();
  453. accumulate_max(maxWidth, _contact->maxWidth());
  454. maxWidth += st::boxPadding.left() + st::boxPadding.right();
  455. const auto width = std::clamp(maxWidth, st::boxWidth, st::boxWideWidth);
  456. const auto available = width
  457. - st::boxPadding.left()
  458. - st::boxPadding.right();
  459. auto height = 0;
  460. if (_comment) {
  461. height += _comment->resizeGetHeight(available);
  462. }
  463. height += _contact->resizeGetHeight(available);
  464. setDimensions(width, height);
  465. _contact->initDimensions();
  466. _submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) {
  467. const auto weak = Ui::MakeWeak(this);
  468. original(m);
  469. if (weak) {
  470. closeBox();
  471. }
  472. };
  473. const auto button = addButton(tr::lng_send_button(), [] {});
  474. button->clicks(
  475. ) | rpl::start_with_next([=](Qt::MouseButton which) {
  476. _submit((which == Qt::RightButton)
  477. ? SkipSwitchModifiers()
  478. : button->clickModifiers());
  479. }, button->lifetime());
  480. button->setAcceptBoth(true);
  481. addButton(tr::lng_cancel(), [=] { closeBox(); });
  482. }
  483. void ConfirmContactBox::keyPressEvent(QKeyEvent *e) {
  484. if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
  485. _submit(e->modifiers());
  486. } else {
  487. BoxContent::keyPressEvent(e);
  488. }
  489. }
  490. void ConfirmContactBox::paintEvent(QPaintEvent *e) {
  491. Painter p(this);
  492. p.fillRect(e->rect(), st::boxBg);
  493. const auto theme = controller()->defaultChatTheme().get();
  494. auto context = theme->preparePaintContext(
  495. _chatStyle.get(),
  496. rect(),
  497. rect(),
  498. controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));
  499. p.translate(st::boxPadding.left(), 0);
  500. if (_comment) {
  501. context.outbg = _comment->hasOutLayout();
  502. _comment->draw(p, context);
  503. p.translate(0, _comment->height());
  504. }
  505. context.outbg = _contact->hasOutLayout();
  506. _contact->draw(p, context);
  507. }
  508. HistoryView::Context ConfirmContactBox::elementContext() {
  509. return HistoryView::Context::ContactPreview;
  510. }
  511. } // namespace Support