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