1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "chat_helpers/bot_keyboard.h"
9
10 #include "core/click_handler_types.h"
11 #include "history/history.h"
12 #include "history/history_item_components.h"
13 #include "data/data_user.h"
14 #include "data/data_session.h"
15 #include "main/main_session.h"
16 #include "window/window_session_controller.h"
17 #include "ui/cached_round_corners.h"
18 #include "facades.h"
19 #include "styles/style_widgets.h"
20 #include "styles/style_chat.h"
21
22 namespace {
23
24 class Style : public ReplyKeyboard::Style {
25 public:
26 Style(
27 not_null<BotKeyboard*> parent,
28 const style::BotKeyboardButton &st);
29
30 int buttonRadius() const override;
31
32 void startPaint(Painter &p, const Ui::ChatStyle *st) const override;
33 const style::TextStyle &textStyle() const override;
34 void repaint(not_null<const HistoryItem*> item) const override;
35
36 protected:
37 void paintButtonBg(
38 Painter &p,
39 const Ui::ChatStyle *st,
40 const QRect &rect,
41 float64 howMuchOver) const override;
42 void paintButtonIcon(
43 Painter &p,
44 const Ui::ChatStyle *st,
45 const QRect &rect,
46 int outerWidth,
47 HistoryMessageMarkupButton::Type type) const override;
48 void paintButtonLoading(
49 Painter &p,
50 const Ui::ChatStyle *st,
51 const QRect &rect) const override;
52 int minButtonWidth(HistoryMessageMarkupButton::Type type) const override;
53
54 private:
55 not_null<BotKeyboard*> _parent;
56
57 };
58
Style(not_null<BotKeyboard * > parent,const style::BotKeyboardButton & st)59 Style::Style(
60 not_null<BotKeyboard*> parent,
61 const style::BotKeyboardButton &st)
62 : ReplyKeyboard::Style(st), _parent(parent) {
63 }
64
startPaint(Painter & p,const Ui::ChatStyle * st) const65 void Style::startPaint(Painter &p, const Ui::ChatStyle *st) const {
66 p.setPen(st::botKbColor);
67 p.setFont(st::botKbStyle.font);
68 }
69
textStyle() const70 const style::TextStyle &Style::textStyle() const {
71 return st::botKbStyle;
72 }
73
repaint(not_null<const HistoryItem * > item) const74 void Style::repaint(not_null<const HistoryItem*> item) const {
75 _parent->update();
76 }
77
buttonRadius() const78 int Style::buttonRadius() const {
79 return st::roundRadiusSmall;
80 }
81
paintButtonBg(Painter & p,const Ui::ChatStyle * st,const QRect & rect,float64 howMuchOver) const82 void Style::paintButtonBg(
83 Painter &p,
84 const Ui::ChatStyle *st,
85 const QRect &rect,
86 float64 howMuchOver) const {
87 Ui::FillRoundRect(p, rect, st::botKbBg, Ui::BotKeyboardCorners);
88 }
89
paintButtonIcon(Painter & p,const Ui::ChatStyle * st,const QRect & rect,int outerWidth,HistoryMessageMarkupButton::Type type) const90 void Style::paintButtonIcon(
91 Painter &p,
92 const Ui::ChatStyle *st,
93 const QRect &rect,
94 int outerWidth,
95 HistoryMessageMarkupButton::Type type) const {
96 // Buttons with icons should not appear here.
97 }
98
paintButtonLoading(Painter & p,const Ui::ChatStyle * st,const QRect & rect) const99 void Style::paintButtonLoading(
100 Painter &p,
101 const Ui::ChatStyle *st,
102 const QRect &rect) const {
103 // Buttons with loading progress should not appear here.
104 }
105
minButtonWidth(HistoryMessageMarkupButton::Type type) const106 int Style::minButtonWidth(HistoryMessageMarkupButton::Type type) const {
107 int result = 2 * buttonPadding();
108 return result;
109 }
110
111 } // namespace
112
BotKeyboard(not_null<Window::SessionController * > controller,QWidget * parent)113 BotKeyboard::BotKeyboard(
114 not_null<Window::SessionController*> controller,
115 QWidget *parent)
116 : TWidget(parent)
117 , _controller(controller)
118 , _st(&st::botKbButton) {
119 setGeometry(0, 0, _st->margin, st::botKbScroll.deltat);
120 _height = st::botKbScroll.deltat;
121 setMouseTracking(true);
122 }
123
paintEvent(QPaintEvent * e)124 void BotKeyboard::paintEvent(QPaintEvent *e) {
125 Painter p(this);
126
127 auto clip = e->rect();
128 p.fillRect(clip, st::historyComposeAreaBg);
129
130 if (_impl) {
131 int x = rtl() ? st::botKbScroll.width : _st->margin;
132 p.translate(x, st::botKbScroll.deltat);
133 _impl->paint(p, nullptr, width(), clip.translated(-x, -st::botKbScroll.deltat));
134 }
135 }
136
mousePressEvent(QMouseEvent * e)137 void BotKeyboard::mousePressEvent(QMouseEvent *e) {
138 _lastMousePos = e->globalPos();
139 updateSelected();
140
141 ClickHandler::pressed();
142 }
143
mouseMoveEvent(QMouseEvent * e)144 void BotKeyboard::mouseMoveEvent(QMouseEvent *e) {
145 _lastMousePos = e->globalPos();
146 updateSelected();
147 }
148
mouseReleaseEvent(QMouseEvent * e)149 void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) {
150 _lastMousePos = e->globalPos();
151 updateSelected();
152
153 if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
154 ActivateClickHandler(window(), activated, {
155 e->button(),
156 QVariant::fromValue(ClickHandlerContext{
157 .sessionWindow = base::make_weak(_controller.get()),
158 })
159 });
160 }
161 }
162
enterEventHook(QEnterEvent * e)163 void BotKeyboard::enterEventHook(QEnterEvent *e) {
164 _lastMousePos = QCursor::pos();
165 updateSelected();
166 }
167
leaveEventHook(QEvent * e)168 void BotKeyboard::leaveEventHook(QEvent *e) {
169 clearSelection();
170 }
171
moderateKeyActivate(int key)172 bool BotKeyboard::moderateKeyActivate(int key) {
173 const auto &data = _controller->session().data();
174
175 const auto botCommand = [](int key) {
176 if (key == Qt::Key_Q || key == Qt::Key_6) {
177 return u"/translate"_q;
178 } else if (key == Qt::Key_W || key == Qt::Key_5) {
179 return u"/eng"_q;
180 } else if (key == Qt::Key_3) {
181 return u"/pattern"_q;
182 } else if (key == Qt::Key_4) {
183 return u"/abuse"_q;
184 } else if (key == Qt::Key_0 || key == Qt::Key_E || key == Qt::Key_9) {
185 return u"/undo"_q;
186 } else if (key == Qt::Key_Plus
187 || key == Qt::Key_QuoteLeft
188 || key == Qt::Key_7) {
189 return u"/next"_q;
190 } else if (key == Qt::Key_Period
191 || key == Qt::Key_S
192 || key == Qt::Key_8) {
193 return u"/stats"_q;
194 }
195 return QString();
196 };
197
198 if (const auto item = data.message(_wasForMsgId)) {
199 if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
200 if (key >= Qt::Key_1 && key <= Qt::Key_2) {
201 const auto index = int(key - Qt::Key_1);
202 if (!markup->data.rows.empty()
203 && index >= 0
204 && index < int(markup->data.rows.front().size())) {
205 App::activateBotCommand(_controller, item, 0, index);
206 return true;
207 }
208 } else if (const auto user = item->history()->peer->asUser()) {
209 if (user->isBot() && item->from() == user) {
210 const auto command = botCommand(key);
211 if (!command.isEmpty()) {
212 _sendCommandRequests.fire({
213 .peer = user,
214 .command = command,
215 .context = item->fullId(),
216 });
217 }
218 return true;
219 }
220 }
221 }
222 }
223 return false;
224 }
225
clickHandlerActiveChanged(const ClickHandlerPtr & p,bool active)226 void BotKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
227 if (!_impl) return;
228 _impl->clickHandlerActiveChanged(p, active);
229 }
230
clickHandlerPressedChanged(const ClickHandlerPtr & p,bool pressed)231 void BotKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
232 if (!_impl) return;
233 _impl->clickHandlerPressedChanged(p, pressed);
234 }
235
updateMarkup(HistoryItem * to,bool force)236 bool BotKeyboard::updateMarkup(HistoryItem *to, bool force) {
237 if (!to || !to->definesReplyKeyboard()) {
238 if (_wasForMsgId.msg) {
239 _maximizeSize = _singleUse = _forceReply = false;
240 _wasForMsgId = FullMsgId();
241 _placeholder = QString();
242 _impl = nullptr;
243 return true;
244 }
245 return false;
246 }
247
248 if (_wasForMsgId == FullMsgId(to->channelId(), to->id) && !force) {
249 return false;
250 }
251
252 _wasForMsgId = FullMsgId(to->channelId(), to->id);
253
254 auto markupFlags = to->replyKeyboardFlags();
255 _forceReply = markupFlags & ReplyMarkupFlag::ForceReply;
256 _maximizeSize = !(markupFlags & ReplyMarkupFlag::Resize);
257 _singleUse = _forceReply || (markupFlags & ReplyMarkupFlag::SingleUse);
258
259 if (const auto markup = to->Get<HistoryMessageReplyMarkup>()) {
260 _placeholder = markup->data.placeholder;
261 } else {
262 _placeholder = QString();
263 }
264
265 _impl = nullptr;
266 if (auto markup = to->Get<HistoryMessageReplyMarkup>()) {
267 if (!markup->data.rows.empty()) {
268 _impl = std::make_unique<ReplyKeyboard>(
269 to,
270 std::make_unique<Style>(this, *_st));
271 }
272 }
273
274 resizeToWidth(width(), _maxOuterHeight);
275
276 return true;
277 }
278
hasMarkup() const279 bool BotKeyboard::hasMarkup() const {
280 return _impl != nullptr;
281 }
282
forceReply() const283 bool BotKeyboard::forceReply() const {
284 return _forceReply;
285 }
286
resizeGetHeight(int newWidth)287 int BotKeyboard::resizeGetHeight(int newWidth) {
288 updateStyle(newWidth);
289 _height = st::botKbScroll.deltat + st::botKbScroll.deltab + (_impl ? _impl->naturalHeight() : 0);
290 if (_maximizeSize) {
291 accumulate_max(_height, _maxOuterHeight);
292 }
293 if (_impl) {
294 int implWidth = newWidth - _st->margin - st::botKbScroll.width;
295 int implHeight = _height - (st::botKbScroll.deltat + st::botKbScroll.deltab);
296 _impl->resize(implWidth, implHeight);
297 }
298 return _height;
299 }
300
maximizeSize() const301 bool BotKeyboard::maximizeSize() const {
302 return _maximizeSize;
303 }
304
singleUse() const305 bool BotKeyboard::singleUse() const {
306 return _singleUse;
307 }
308
updateStyle(int newWidth)309 void BotKeyboard::updateStyle(int newWidth) {
310 if (!_impl) return;
311
312 int implWidth = newWidth - st::botKbButton.margin - st::botKbScroll.width;
313 _st = _impl->isEnoughSpace(implWidth, st::botKbButton) ? &st::botKbButton : &st::botKbTinyButton;
314
315 _impl->setStyle(std::make_unique<Style>(this, *_st));
316 }
317
clearSelection()318 void BotKeyboard::clearSelection() {
319 if (_impl) {
320 if (ClickHandler::setActive(ClickHandlerPtr(), this)) {
321 Ui::Tooltip::Hide();
322 setCursor(style::cur_default);
323 }
324 }
325 }
326
tooltipPos() const327 QPoint BotKeyboard::tooltipPos() const {
328 return _lastMousePos;
329 }
330
tooltipWindowActive() const331 bool BotKeyboard::tooltipWindowActive() const {
332 return Ui::AppInFocus() && Ui::InFocusChain(window());
333 }
334
tooltipText() const335 QString BotKeyboard::tooltipText() const {
336 if (ClickHandlerPtr lnk = ClickHandler::getActive()) {
337 return lnk->tooltip();
338 }
339 return QString();
340 }
341
updateSelected()342 void BotKeyboard::updateSelected() {
343 Ui::Tooltip::Show(1000, this);
344
345 if (!_impl) return;
346
347 auto p = mapFromGlobal(_lastMousePos);
348 auto x = rtl() ? st::botKbScroll.width : _st->margin;
349
350 auto link = _impl->getLink(p - QPoint(x, _st->margin));
351 if (ClickHandler::setActive(link, this)) {
352 Ui::Tooltip::Hide();
353 setCursor(link ? style::cur_pointer : style::cur_default);
354 }
355 }
356
sendCommandRequests() const357 auto BotKeyboard::sendCommandRequests() const
358 -> rpl::producer<Bot::SendCommandRequest> {
359 return _sendCommandRequests.events();
360 }
361
362 BotKeyboard::~BotKeyboard() = default;
363