/* 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 "chat_helpers/bot_keyboard.h" #include "core/click_handler_types.h" #include "history/history.h" #include "history/history_item_components.h" #include "data/data_user.h" #include "data/data_session.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "api/api_bot.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" namespace { class Style : public ReplyKeyboard::Style { public: Style( not_null parent, const style::BotKeyboardButton &st); int buttonRadius() const override; void startPaint(QPainter &p, const Ui::ChatStyle *st) const override; const style::TextStyle &textStyle() const override; void repaint(not_null item) const override; protected: void paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, float64 howMuchOver) const override; void paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, HistoryMessageMarkupButton::Type type) const override; void paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; private: not_null _parent; }; Style::Style( not_null parent, const style::BotKeyboardButton &st) : ReplyKeyboard::Style(st), _parent(parent) { } void Style::startPaint(QPainter &p, const Ui::ChatStyle *st) const { p.setPen(st::botKbColor); p.setFont(st::botKbStyle.font); } const style::TextStyle &Style::textStyle() const { return st::botKbStyle; } void Style::repaint(not_null item) const { _parent->update(); } int Style::buttonRadius() const { return st::roundRadiusSmall; } void Style::paintButtonBg( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, float64 howMuchOver) const { Ui::FillRoundRect(p, rect, st::botKbBg, Ui::BotKeyboardCorners); } void Style::paintButtonIcon( QPainter &p, const Ui::ChatStyle *st, const QRect &rect, int outerWidth, HistoryMessageMarkupButton::Type type) const { // Buttons with icons should not appear here. } void Style::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, const QRect &rect) const { // Buttons with loading progress should not appear here. } int Style::minButtonWidth(HistoryMessageMarkupButton::Type type) const { int result = 2 * buttonPadding(); return result; } } // namespace BotKeyboard::BotKeyboard( not_null controller, QWidget *parent) : TWidget(parent) , _controller(controller) , _st(&st::botKbButton) { setGeometry(0, 0, _st->margin, st::botKbScroll.deltat); _height = st::botKbScroll.deltat; setMouseTracking(true); } void BotKeyboard::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); p.fillRect(clip, st::historyComposeAreaBg); if (_impl) { int x = rtl() ? st::botKbScroll.width : _st->margin; p.translate(x, st::botKbScroll.deltat); _impl->paint(p, nullptr, width(), clip.translated(-x, -st::botKbScroll.deltat)); } } void BotKeyboard::mousePressEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); ClickHandler::pressed(); } void BotKeyboard::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); } void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); if (ClickHandlerPtr activated = ClickHandler::unpressed()) { ActivateClickHandler(window(), activated, { e->button(), QVariant::fromValue(ClickHandlerContext{ .sessionWindow = base::make_weak(_controller.get()), }) }); } } void BotKeyboard::enterEventHook(QEnterEvent *e) { _lastMousePos = QCursor::pos(); updateSelected(); } void BotKeyboard::leaveEventHook(QEvent *e) { clearSelection(); } bool BotKeyboard::moderateKeyActivate( int key, Fn context) { const auto &data = _controller->session().data(); const auto botCommand = [](int key) { if (key == Qt::Key_Q || key == Qt::Key_6) { return u"/translate"_q; } else if (key == Qt::Key_W || key == Qt::Key_5) { return u"/eng"_q; } else if (key == Qt::Key_3) { return u"/pattern"_q; } else if (key == Qt::Key_4) { return u"/abuse"_q; } else if (key == Qt::Key_0 || key == Qt::Key_E || key == Qt::Key_9) { return u"/undo"_q; } else if (key == Qt::Key_Plus || key == Qt::Key_QuoteLeft || key == Qt::Key_7) { return u"/next"_q; } else if (key == Qt::Key_Period || key == Qt::Key_S || key == Qt::Key_8) { return u"/stats"_q; } return QString(); }; if (const auto item = data.message(_wasForMsgId)) { if (const auto markup = item->Get()) { if (key >= Qt::Key_1 && key <= Qt::Key_2) { const auto index = int(key - Qt::Key_1); if (!markup->data.rows.empty() && index >= 0 && index < int(markup->data.rows.front().size())) { Api::ActivateBotCommand( context( _wasForMsgId).other.value(), 0, index); return true; } } else if (const auto user = item->history()->peer->asUser()) { if (user->isBot() && item->from() == user) { const auto command = botCommand(key); if (!command.isEmpty()) { _sendCommandRequests.fire({ .peer = user, .command = command, .context = item->fullId(), }); } return true; } } } } return false; } void BotKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!_impl) return; _impl->clickHandlerActiveChanged(p, active); } void BotKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (!_impl) return; _impl->clickHandlerPressedChanged(p, pressed); } bool BotKeyboard::updateMarkup(HistoryItem *to, bool force) { if (!to || !to->definesReplyKeyboard()) { if (_wasForMsgId.msg) { _maximizeSize = _singleUse = _forceReply = false; _wasForMsgId = FullMsgId(); _placeholder = QString(); _impl = nullptr; return true; } return false; } const auto peerId = to->history()->peer->id; if (_wasForMsgId == FullMsgId(peerId, to->id) && !force) { return false; } _wasForMsgId = FullMsgId(peerId, to->id); auto markupFlags = to->replyKeyboardFlags(); _forceReply = markupFlags & ReplyMarkupFlag::ForceReply; _maximizeSize = !(markupFlags & ReplyMarkupFlag::Resize); _singleUse = _forceReply || (markupFlags & ReplyMarkupFlag::SingleUse); if (const auto markup = to->Get()) { _placeholder = markup->data.placeholder; } else { _placeholder = QString(); } _impl = nullptr; if (auto markup = to->Get()) { if (!markup->data.rows.empty()) { _impl = std::make_unique( to, std::make_unique