Add FieldAutocomplete to ComposeControls.

This commit is contained in:
John Preston 2020-11-10 17:10:08 +03:00
parent 5d2ffae215
commit ac02e2be9e
9 changed files with 444 additions and 199 deletions

View file

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_stickers.h"
#include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu
#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/message_field.h" // PrepareMentionTag.
#include "mainwindow.h"
#include "apiwrap.h"
#include "main/main_session.h"
@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lottie/lottie_single_player.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/input_fields.h"
#include "ui/image/image.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
@ -39,15 +41,105 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtWidgets/QApplication>
class FieldAutocomplete::Inner final
: public Ui::RpWidget
, private base::Subscriber {
public:
struct ScrollTo {
int top;
int bottom;
};
Inner(
not_null<Window::SessionController*> controller,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
not_null<HashtagRows*> hrows,
not_null<BotCommandRows*> brows,
not_null<StickerRows*> srows);
void clearSel(bool hidden = false);
bool moveSel(int key);
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
bool chooseAtIndex(
FieldAutocomplete::ChooseMethod method,
int index,
Api::SendOptions options = Api::SendOptions()) const;
void setRecentInlineBotsInRows(int32 bots);
void rowsUpdated();
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
rpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;
rpl::producer<FieldAutocomplete::BotCommandChosen>
botCommandChosen() const;
rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
rpl::producer<ScrollTo> scrollToRequested() const;
void onParentGeometryChanged();
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void enterEventHook(QEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void updateSelectedRow();
void setSel(int sel, bool scroll = false);
void showPreview();
void selectByMouse(QPoint global);
QSize stickerBoundingBox() const;
void setupLottie(StickerSuggestion &suggestion);
void repaintSticker(not_null<DocumentData*> document);
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
const not_null<Window::SessionController*> _controller;
const not_null<FieldAutocomplete*> _parent;
const not_null<MentionRows*> _mrows;
const not_null<HashtagRows*> _hrows;
const not_null<BotCommandRows*> _brows;
const not_null<StickerRows*> _srows;
rpl::lifetime _stickersLifetime;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
base::unique_qptr<Ui::PopupMenu> _menu;
int _stickersPerRow = 1;
int _recentInlineBotsInRows = 0;
int _sel = -1;
int _down = -1;
std::optional<QPoint> _lastMousePosition;
bool _mouseSelection = false;
bool _overDelete = false;
bool _previewShown = false;
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
rpl::event_stream<ScrollTo> _scrollToRequested;
base::Timer _previewTimer;
};
FieldAutocomplete::FieldAutocomplete(
QWidget *parent,
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _controller(controller)
, _scroll(this, st::mentionScroll) {
_scroll->setGeometry(rect());
hide();
using Inner = internal::FieldAutocompleteInner;
_scroll->setGeometry(rect());
_inner = _scroll->setOwnedWidget(
object_ptr<Inner>(
@ -76,6 +168,10 @@ FieldAutocomplete::FieldAutocomplete(
&Inner::onParentGeometryChanged);
}
not_null<Window::SessionController*> FieldAutocomplete::controller() const {
return _controller;
}
auto FieldAutocomplete::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> {
return _inner->mentionChosen();
@ -125,9 +221,9 @@ void FieldAutocomplete::showFiltered(
if (query.isEmpty()) {
_type = Type::Mentions;
rowsUpdated(
internal::MentionRows(),
internal::HashtagRows(),
internal::BotCommandRows(),
MentionRows(),
HashtagRows(),
BotCommandRows(),
base::take(_srows),
false);
return;
@ -171,7 +267,7 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
base::take(_mrows),
base::take(_hrows),
base::take(_brows),
internal::StickerRows(),
StickerRows(),
false);
return;
}
@ -203,7 +299,7 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) {
}
}
internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
const auto list = _controller->session().data().stickers().getListByEmoji(
_emoji,
_stickersSeed
@ -211,7 +307,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
auto result = ranges::view::all(
list
) | ranges::view::transform([](not_null<DocumentData*> sticker) {
return internal::StickerSuggestion{
return StickerSuggestion{
sticker,
sticker->createMediaView()
};
@ -223,7 +319,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
const auto i = ranges::find(
result,
suggestion.document,
&internal::StickerSuggestion::document);
&StickerSuggestion::document);
if (i != end(result)) {
i->animated = std::move(suggestion.animated);
}
@ -233,10 +329,10 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
void FieldAutocomplete::updateFiltered(bool resetScroll) {
int32 now = base::unixtime::now(), recentInlineBots = 0;
internal::MentionRows mrows;
internal::HashtagRows hrows;
internal::BotCommandRows brows;
internal::StickerRows srows;
MentionRows mrows;
HashtagRows hrows;
BotCommandRows brows;
StickerRows srows;
if (_emoji) {
srows = getStickerSuggestions();
} else if (_type == Type::Mentions) {
@ -435,10 +531,10 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
}
void FieldAutocomplete::rowsUpdated(
internal::MentionRows &&mrows,
internal::HashtagRows &&hrows,
internal::BotCommandRows &&brows,
internal::StickerRows &&srows,
MentionRows &&mrows,
HashtagRows &&hrows,
BotCommandRows &&brows,
StickerRows &&srows,
bool resetScroll) {
if (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) {
if (!isHidden()) {
@ -620,9 +716,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
return QWidget::eventFilter(obj, e);
}
namespace internal {
FieldAutocompleteInner::FieldAutocompleteInner(
FieldAutocomplete::Inner::Inner(
not_null<Window::SessionController*> controller,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
@ -642,7 +736,7 @@ FieldAutocompleteInner::FieldAutocompleteInner(
}, lifetime());
}
void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
QRect r(e->rect());
@ -841,11 +935,11 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowFg);
}
void FieldAutocompleteInner::resizeEvent(QResizeEvent *e) {
void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {
_stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));
}
void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) {
void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
const auto globalPosition = e->globalPos();
if (!_lastMousePosition) {
_lastMousePosition = globalPosition;
@ -857,7 +951,7 @@ void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) {
selectByMouse(globalPosition);
}
void FieldAutocompleteInner::clearSel(bool hidden) {
void FieldAutocomplete::Inner::clearSel(bool hidden) {
_overDelete = false;
_mouseSelection = false;
_lastMousePosition = std::nullopt;
@ -868,7 +962,7 @@ void FieldAutocompleteInner::clearSel(bool hidden) {
}
}
bool FieldAutocompleteInner::moveSel(int key) {
bool FieldAutocomplete::Inner::moveSel(int key) {
_mouseSelection = false;
_lastMousePosition = std::nullopt;
@ -903,12 +997,12 @@ bool FieldAutocompleteInner::moveSel(int key) {
return true;
}
bool FieldAutocompleteInner::chooseSelected(
bool FieldAutocomplete::Inner::chooseSelected(
FieldAutocomplete::ChooseMethod method) const {
return chooseAtIndex(method, _sel);
}
bool FieldAutocompleteInner::chooseAtIndex(
bool FieldAutocomplete::Inner::chooseAtIndex(
FieldAutocomplete::ChooseMethod method,
int index,
Api::SendOptions options) const {
@ -955,11 +1049,11 @@ bool FieldAutocompleteInner::chooseAtIndex(
return false;
}
void FieldAutocompleteInner::setRecentInlineBotsInRows(int32 bots) {
void FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) {
_recentInlineBotsInRows = bots;
}
void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) {
void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
selectByMouse(e->globalPos());
if (e->button() == Qt::LeftButton) {
if (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) {
@ -999,7 +1093,7 @@ void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) {
}
}
void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) {
void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {
_previewTimer.cancel();
int32 pressed = _down;
@ -1017,7 +1111,7 @@ void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) {
chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
}
void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) {
void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
if (_sel < 0 || _srows->empty() || _down >= 0) {
return;
}
@ -1040,11 +1134,11 @@ void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) {
}
}
void FieldAutocompleteInner::enterEventHook(QEvent *e) {
void FieldAutocomplete::Inner::enterEventHook(QEvent *e) {
setMouseTracking(true);
}
void FieldAutocompleteInner::leaveEventHook(QEvent *e) {
void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {
setMouseTracking(false);
if (_mouseSelection) {
setSel(-1);
@ -1053,7 +1147,7 @@ void FieldAutocompleteInner::leaveEventHook(QEvent *e) {
}
}
void FieldAutocompleteInner::updateSelectedRow() {
void FieldAutocomplete::Inner::updateSelectedRow() {
if (_sel >= 0) {
if (_srows->empty()) {
update(0, _sel * st::mentionHeight, width(), st::mentionHeight);
@ -1064,7 +1158,7 @@ void FieldAutocompleteInner::updateSelectedRow() {
}
}
void FieldAutocompleteInner::setSel(int sel, bool scroll) {
void FieldAutocomplete::Inner::setSel(int sel, bool scroll) {
updateSelectedRow();
_sel = sel;
updateSelectedRow();
@ -1084,13 +1178,13 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) {
}
}
void FieldAutocompleteInner::rowsUpdated() {
void FieldAutocomplete::Inner::rowsUpdated() {
if (_srows->empty()) {
_stickersLifetime.destroy();
}
}
auto FieldAutocompleteInner::getLottieRenderer()
auto FieldAutocomplete::Inner::getLottieRenderer()
-> std::shared_ptr<Lottie::FrameRenderer> {
if (auto result = _lottieRenderer.lock()) {
return result;
@ -1100,7 +1194,7 @@ auto FieldAutocompleteInner::getLottieRenderer()
return result;
}
void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) {
void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
const auto document = suggestion.document;
suggestion.animated = ChatHelpers::LottiePlayerFromDocument(
suggestion.documentMedia.get(),
@ -1115,13 +1209,13 @@ void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) {
}, _stickersLifetime);
}
QSize FieldAutocompleteInner::stickerBoundingBox() const {
QSize FieldAutocomplete::Inner::stickerBoundingBox() const {
return QSize(
st::stickerPanSize.width() - st::buttonRadius * 2,
st::stickerPanSize.height() - st::buttonRadius * 2);
}
void FieldAutocompleteInner::repaintSticker(
void FieldAutocomplete::Inner::repaintSticker(
not_null<DocumentData*> document) {
const auto i = ranges::find(
*_srows,
@ -1140,7 +1234,7 @@ void FieldAutocompleteInner::repaintSticker(
st::stickerPanSize.height());
}
void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {
_mouseSelection = true;
_lastMousePosition = globalPosition;
const auto mouse = mapFromGlobal(globalPosition);
@ -1186,7 +1280,7 @@ void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
}
}
void FieldAutocompleteInner::onParentGeometryChanged() {
void FieldAutocomplete::Inner::onParentGeometryChanged() {
const auto globalPosition = QCursor::pos();
if (rect().contains(mapFromGlobal(globalPosition))) {
setMouseTracking(true);
@ -1196,7 +1290,7 @@ void FieldAutocompleteInner::onParentGeometryChanged() {
}
}
void FieldAutocompleteInner::showPreview() {
void FieldAutocomplete::Inner::showPreview() {
if (_down >= 0 && _down < _srows->size()) {
if (const auto w = App::wnd()) {
w->showMediaPreview(
@ -1207,29 +1301,27 @@ void FieldAutocompleteInner::showPreview() {
}
}
auto FieldAutocompleteInner::mentionChosen() const
auto FieldAutocomplete::Inner::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> {
return _mentionChosen.events();
}
auto FieldAutocompleteInner::hashtagChosen() const
auto FieldAutocomplete::Inner::hashtagChosen() const
-> rpl::producer<FieldAutocomplete::HashtagChosen> {
return _hashtagChosen.events();
}
auto FieldAutocompleteInner::botCommandChosen() const
auto FieldAutocomplete::Inner::botCommandChosen() const
-> rpl::producer<FieldAutocomplete::BotCommandChosen> {
return _botCommandChosen.events();
}
auto FieldAutocompleteInner::stickerChosen() const
auto FieldAutocomplete::Inner::stickerChosen() const
-> rpl::producer<FieldAutocomplete::StickerChosen> {
return _stickerChosen.events();
}
auto FieldAutocompleteInner::scrollToRequested() const
auto FieldAutocomplete::Inner::scrollToRequested() const
-> rpl::producer<ScrollTo> {
return _scrollToRequested.events();
}
} // namespace internal

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class PopupMenu;
class ScrollArea;
class InputField;
} // namespace Ui
namespace Lottie {
@ -32,42 +33,15 @@ class DocumentMedia;
class CloudImageView;
} // namespace Data
namespace internal {
struct StickerSuggestion {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
std::unique_ptr<Lottie::SinglePlayer> animated;
};
struct MentionRow {
not_null<UserData*> user;
std::shared_ptr<Data::CloudImageView> userpic;
};
struct BotCommandRow {
not_null<UserData*> user;
not_null<const BotCommand*> command;
std::shared_ptr<Data::CloudImageView> userpic;
};
using HashtagRows = std::vector<QString>;
using BotCommandRows = std::vector<BotCommandRow>;
using StickerRows = std::vector<StickerSuggestion>;
using MentionRows = std::vector<MentionRow>;
class FieldAutocompleteInner;
} // namespace internal
class FieldAutocomplete final : public Ui::RpWidget {
public:
FieldAutocomplete(
QWidget *parent,
not_null<Window::SessionController*> controller);
~FieldAutocomplete();
[[nodiscard]] not_null<Window::SessionController*> controller() const;
bool clearFilteredBotCommands();
void showFiltered(
not_null<PeerData*> peer,
@ -140,29 +114,54 @@ protected:
void paintEvent(QPaintEvent *e) override;
private:
class Inner;
friend class Inner;
struct StickerSuggestion {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
std::unique_ptr<Lottie::SinglePlayer> animated;
};
struct MentionRow {
not_null<UserData*> user;
std::shared_ptr<Data::CloudImageView> userpic;
};
struct BotCommandRow {
not_null<UserData*> user;
not_null<const BotCommand*> command;
std::shared_ptr<Data::CloudImageView> userpic;
};
using HashtagRows = std::vector<QString>;
using BotCommandRows = std::vector<BotCommandRow>;
using StickerRows = std::vector<StickerSuggestion>;
using MentionRows = std::vector<MentionRow>;
void animationCallback();
void hideFinish();
void updateFiltered(bool resetScroll = false);
void recount(bool resetScroll = false);
internal::StickerRows getStickerSuggestions();
StickerRows getStickerSuggestions();
const not_null<Window::SessionController*> _controller;
QPixmap _cache;
internal::MentionRows _mrows;
internal::HashtagRows _hrows;
internal::BotCommandRows _brows;
internal::StickerRows _srows;
MentionRows _mrows;
HashtagRows _hrows;
BotCommandRows _brows;
StickerRows _srows;
void rowsUpdated(
internal::MentionRows &&mrows,
internal::HashtagRows &&hrows,
internal::BotCommandRows &&brows,
internal::StickerRows &&srows,
MentionRows &&mrows,
HashtagRows &&hrows,
BotCommandRows &&brows,
StickerRows &&srows,
bool resetScroll);
object_ptr<Ui::ScrollArea> _scroll;
QPointer<internal::FieldAutocompleteInner> _inner;
QPointer<Inner> _inner;
ChatData *_chat = nullptr;
UserData *_user = nullptr;
@ -186,100 +185,4 @@ private:
Fn<bool(int)> _moderateKeyActivateCallback;
friend class internal::FieldAutocompleteInner;
};
namespace internal {
class FieldAutocompleteInner final
: public Ui::RpWidget
, private base::Subscriber {
public:
struct ScrollTo {
int top;
int bottom;
};
FieldAutocompleteInner(
not_null<Window::SessionController*> controller,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
not_null<HashtagRows*> hrows,
not_null<BotCommandRows*> brows,
not_null<StickerRows*> srows);
void clearSel(bool hidden = false);
bool moveSel(int key);
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
bool chooseAtIndex(
FieldAutocomplete::ChooseMethod method,
int index,
Api::SendOptions options = Api::SendOptions()) const;
void setRecentInlineBotsInRows(int32 bots);
void rowsUpdated();
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
rpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;
rpl::producer<FieldAutocomplete::BotCommandChosen>
botCommandChosen() const;
rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
rpl::producer<ScrollTo> scrollToRequested() const;
void onParentGeometryChanged();
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void enterEventHook(QEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void updateSelectedRow();
void setSel(int sel, bool scroll = false);
void showPreview();
void selectByMouse(QPoint global);
QSize stickerBoundingBox() const;
void setupLottie(StickerSuggestion &suggestion);
void repaintSticker(not_null<DocumentData*> document);
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
const not_null<Window::SessionController*> _controller;
const not_null<FieldAutocomplete*> _parent;
const not_null<MentionRows*> _mrows;
const not_null<HashtagRows*> _hrows;
const not_null<BotCommandRows*> _brows;
const not_null<StickerRows*> _srows;
rpl::lifetime _stickersLifetime;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
base::unique_qptr<Ui::PopupMenu> _menu;
int _stickersPerRow = 1;
int _recentInlineBotsInRows = 0;
int _sel = -1;
int _down = -1;
std::optional<QPoint> _lastMousePosition;
bool _mouseSelection = false;
bool _overDelete = false;
bool _previewShown = false;
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
rpl::event_stream<ScrollTo> _scrollToRequested;
base::Timer _previewTimer;
};
} // namespace internal

View file

@ -286,7 +286,6 @@ HistoryWidget::HistoryWidget(
_unreadMentions->installEventFilter(this);
InitMessageField(controller, _field);
_fieldAutocomplete->hide();
_fieldAutocomplete->mentionChosen(
) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) {
@ -1216,6 +1215,9 @@ void HistoryWidget::orderWidgets() {
_pinnedBar->raise();
}
_topShadow->raise();
if (_fieldAutocomplete) {
_fieldAutocomplete->raise();
}
if (_membersDropdown) {
_membersDropdown->raise();
}

View file

@ -15,12 +15,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_section.h"
#include "chat_helpers/tabbed_selector.h"
#include "chat_helpers/field_autocomplete.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_changes.h"
#include "data/data_messages.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_stickers.h"
#include "data/data_web_page.h"
#include "storage/storage_account.h"
#include "facades.h"
#include "boxes/confirm_box.h"
#include "history/history.h"
@ -515,6 +519,9 @@ ComposeControls::ComposeControls(
st::historyComposeField,
Ui::InputField::Mode::MultiLine,
tr::lng_message_ph()))
, _autocomplete(std::make_unique<FieldAutocomplete>(
parent,
window))
, _header(std::make_unique<FieldHeader>(
_wrap.get(),
&_window->session().data()))
@ -565,6 +572,12 @@ void ComposeControls::resizeToWidth(int width) {
updateHeight();
}
void ComposeControls::setAutocompleteBoundingRect(QRect rect) {
if (_autocomplete) {
_autocomplete->setBoundings(rect);
}
}
rpl::producer<int> ComposeControls::height() const {
using namespace rpl::mappers;
return rpl::conditional(
@ -619,6 +632,10 @@ rpl::producer<VoiceToSend> ComposeControls::sendVoiceRequests() const {
return _voiceRecordBar->sendVoiceRequests();
}
rpl::producer<QString> ComposeControls::sendCommandRequests() const {
return _sendCommandRequests.events();
}
rpl::producer<MessageToEdit> ComposeControls::editRequests() const {
auto toValue = rpl::map([=] { return _header->queryToEdit(); });
auto filter = rpl::filter([=] {
@ -681,6 +698,21 @@ void ComposeControls::showFinished() {
_voiceRecordBar->orderControls();
}
void ComposeControls::raisePanels() {
if (_autocomplete) {
_autocomplete->raise();
}
if (_inlineResults) {
_inlineResults->raise();
}
if (_tabbedPanel) {
_tabbedPanel->raise();
}
if (_raiseEmojiSuggestions) {
_raiseEmojiSuggestions();
}
}
void ComposeControls::showForGrab() {
showFinished();
}
@ -708,7 +740,9 @@ void ComposeControls::setText(const TextWithTags &textWithTags) {
}
void ComposeControls::hidePanelsAnimated() {
//_fieldAutocomplete->hideAnimated();
if (_autocomplete) {
_autocomplete->hideAnimated();
}
if (_tabbedPanel) {
_tabbedPanel->hideAnimated();
}
@ -717,6 +751,36 @@ void ComposeControls::hidePanelsAnimated() {
}
}
void ComposeControls::checkAutocomplete() {
if (!_history) {
return;
}
const auto peer = _history->peer;
const auto isInlineBot = false;// _inlineBot && !_inlineLookingUpBot;
const auto autocomplete = isInlineBot
? AutocompleteQuery()
: ParseMentionHashtagBotCommandQuery(_field);
if (!autocomplete.query.isEmpty()) {
if (autocomplete.query[0] == '#'
&& cRecentWriteHashtags().isEmpty()
&& cRecentSearchHashtags().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (autocomplete.query[0] == '@'
&& cRecentInlineBots().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (autocomplete.query[0] == '/'
&& peer->isUser()
&& !peer->asUser()->isBot()) {
return;
}
}
_autocomplete->showFiltered(
peer,
autocomplete.query,
autocomplete.fromStart);
}
void ComposeControls::init() {
initField();
initTabbedSelector();
@ -817,11 +881,12 @@ void ComposeControls::initField() {
_field->setSubmitSettings(Core::App().settings().sendSubmitWay());
//Ui::Connect(_field, &Ui::InputField::submitted, [=] { send(); });
Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); });
//Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); });
Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); });
Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); });
//Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); });
Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); });
InitMessageField(_window, _field);
initAutocomplete();
const auto suggestions = Ui::Emoji::SuggestionsController::Init(
_parent,
_field,
@ -830,6 +895,112 @@ void ComposeControls::initField() {
InitSpellchecker(_window, _field);
}
void ComposeControls::initAutocomplete() {
const auto insertHashtagOrBotCommand = [=](
const QString &string,
FieldAutocomplete::ChooseMethod method) {
// Send bot command at once, if it was not inserted by pressing Tab.
if (string.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) {
_sendCommandRequests.fire_copy(string);
setText(
_field->getTextWithTagsPart(_field->textCursor().position()));
} else {
_field->insertTag(string);
}
};
const auto insertMention = [=](not_null<UserData*> user) {
auto replacement = QString();
auto entityTag = QString();
if (user->username.isEmpty()) {
_field->insertTag(
user->firstName.isEmpty() ? user->name : user->firstName,
PrepareMentionTag(user));
} else {
_field->insertTag('@' + user->username);
}
};
_autocomplete->mentionChosen(
) | rpl::start_with_next([=](FieldAutocomplete::MentionChosen data) {
insertMention(data.user);
}, _autocomplete->lifetime());
_autocomplete->hashtagChosen(
) | rpl::start_with_next([=](FieldAutocomplete::HashtagChosen data) {
insertHashtagOrBotCommand(data.hashtag, data.method);
}, _autocomplete->lifetime());
_autocomplete->botCommandChosen(
) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) {
insertHashtagOrBotCommand(data.command, data.method);
}, _autocomplete->lifetime());
_autocomplete->stickerChosen(
) | rpl::start_with_next([=](FieldAutocomplete::StickerChosen data) {
setText({});
//_saveDraftText = true;
//_saveDraftStart = crl::now();
//onDraftSave();
//onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft
_fileChosen.fire(FileChosen{
.document = data.sticker,
.options = data.options,
});
}, _autocomplete->lifetime());
//_autocomplete->setModerateKeyActivateCallback([=](int key) {
// return _keyboard->isHidden()
// ? false
// : _keyboard->moderateKeyActivate(key);
//});
_field->rawTextEdit()->installEventFilter(_autocomplete.get());
_window->session().data().botCommandsChanges(
) | rpl::filter([=](not_null<UserData*> user) {
const auto peer = _history ? _history->peer.get() : nullptr;
return peer && (peer == user || !peer->isUser());
}) | rpl::start_with_next([=](not_null<UserData*> user) {
if (_autocomplete->clearFilteredBotCommands()) {
checkAutocomplete();
}
}, _autocomplete->lifetime());
_window->session().data().stickers().updated(
) | rpl::start_with_next([=] {
updateStickersByEmoji();
}, _autocomplete->lifetime());
QObject::connect(
_field->rawTextEdit(),
&QTextEdit::cursorPositionChanged,
_autocomplete.get(),
[=] { checkAutocomplete(); },
Qt::QueuedConnection);
}
void ComposeControls::updateStickersByEmoji() {
if (!_history) {
return;
}
const auto emoji = [&] {
const auto errorForStickers = Data::RestrictionError(
_history->peer,
ChatRestriction::f_send_stickers);
if (!isEditingMessage() && !errorForStickers) {
const auto &text = _field->getTextWithTags().text;
auto length = 0;
if (const auto emoji = Ui::Emoji::Find(text, &length)) {
if (text.size() <= length) {
return emoji;
}
}
}
return EmojiPtr(nullptr);
}();
_autocomplete->showStickers(emoji);
}
void ComposeControls::fieldChanged() {
if (/*!_inlineBot
&& */!_header->isEditingMessage()
@ -840,6 +1011,15 @@ void ComposeControls::fieldChanged() {
if (showRecordButton()) {
//_previewCancelled = false;
}
InvokeQueued(_autocomplete.get(), [=] {
updateStickersByEmoji();
});
}
void ComposeControls::fieldTabbed() {
if (!_autocomplete->isHidden()) {
_autocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
}
}
rpl::producer<SendActionUpdate> ComposeControls::sendActionUpdates() const {
@ -1136,10 +1316,16 @@ void ComposeControls::updateHeight() {
void ComposeControls::editMessage(FullMsgId id) {
cancelEditMessage();
_header->editMessage(id);
if (_autocomplete) {
InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); });
}
}
void ComposeControls::cancelEditMessage() {
_header->editMessage({});
if (_autocomplete) {
InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); });
}
}
void ComposeControls::replyToMessage(FullMsgId id) {
@ -1151,6 +1337,20 @@ void ComposeControls::cancelReplyMessage() {
_header->replyToMessage({});
}
bool ComposeControls::handleCancelRequest() {
if (isEditingMessage()) {
cancelEditMessage();
return true;
} else if (_autocomplete && !_autocomplete->isHidden()) {
_autocomplete->hideAnimated();
return true;
} else if (replyingToMessage()) {
cancelReplyMessage();
return true;
}
return false;
}
void ComposeControls::initWebpageProcess() {
Expects(_history);
const auto peer = _history->peer;
@ -1287,7 +1487,10 @@ void ComposeControls::initWebpageProcess() {
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer.get() == peer);
}) | rpl::start_with_next(checkPreview, lifetime);
}) | rpl::start_with_next([=] {
checkPreview();
updateStickersByEmoji();
}, lifetime);
_window->session().downloaderTaskFinished(
) | rpl::filter([=] {

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_selector.h"
class History;
class FieldAutocomplete;
namespace ChatHelpers {
class TabbedPanel;
@ -89,6 +90,7 @@ public:
void move(int x, int y);
void resizeToWidth(int width);
void setAutocompleteBoundingRect(QRect rect);
[[nodiscard]] rpl::producer<int> height() const;
[[nodiscard]] int heightCurrent() const;
@ -96,6 +98,7 @@ public:
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<> sendRequests() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<QString> sendCommandRequests() const;
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
[[nodiscard]] rpl::producer<> attachRequests() const;
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
@ -122,6 +125,7 @@ public:
void showForGrab();
void showStarted();
void showFinished();
void raisePanels();
void editMessage(FullMsgId id);
void cancelEditMessage();
@ -129,6 +133,8 @@ public:
void replyToMessage(FullMsgId id);
void cancelReplyMessage();
bool handleCancelRequest();
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
[[nodiscard]] WebPageId webPageId() const;
void setText(const TextWithTags &text);
@ -154,6 +160,7 @@ private:
void initWebpageProcess();
void initWriteRestriction();
void initVoiceRecordBar();
void initAutocomplete();
void updateSendButtonType();
void updateHeight();
void updateWrappingVisibility();
@ -163,9 +170,12 @@ private:
void paintBackground(QRect clip);
void orderControls();
void checkAutocomplete();
void updateStickersByEmoji();
void escape();
void fieldChanged();
void fieldTabbed();
void toggleTabbedSelectorMode();
void createTabbedPanel();
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
@ -194,6 +204,7 @@ private:
const not_null<Ui::InputField*> _field;
std::unique_ptr<InlineBots::Layout::Widget> _inlineResults;
std::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;
std::unique_ptr<FieldAutocomplete> _autocomplete;
friend class FieldHeader;
const std::unique_ptr<FieldHeader> _header;
@ -204,6 +215,7 @@ private:
rpl::event_stream<PhotoChosen> _photoChosen;
rpl::event_stream<ChatHelpers::TabbedSelector::InlineChosen> _inlineResultChosen;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<QString> _sendCommandRequests;
TextWithTags _localSavedText;
TextUpdateEvents _textUpdateEvents;

View file

@ -253,6 +253,7 @@ RepliesWidget::RepliesWidget(
setupScrollDownButton();
setupComposeControls();
orderWidgets();
}
RepliesWidget::~RepliesWidget() {
@ -263,6 +264,17 @@ RepliesWidget::~RepliesWidget() {
_history->owner().repliesSendActionPainterRemoved(_history, _rootId);
}
void RepliesWidget::orderWidgets() {
if (_topBar) {
_topBar->raise();
}
if (_rootView) {
_rootView->raise();
}
_topBarShadow->raise();
_composeControls->raisePanels();
}
void RepliesWidget::sendReadTillRequest() {
if (!_root) {
_readRequestPending = true;
@ -428,6 +440,18 @@ void RepliesWidget::setupComposeControls() {
sendVoice(data.bytes, data.waveform, data.duration);
}, lifetime());
_composeControls->sendCommandRequests(
) | rpl::start_with_next([=](const QString &command) {
if (showSlowmodeError()) {
return;
}
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { command };
message.action.replyTo = replyToId();
session().api().sendMessage(std::move(message));
finishSending();
}, lifetime());
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
_composeControls->editRequests(
) | rpl::start_with_next([=](auto data) {
@ -1474,6 +1498,7 @@ void RepliesWidget::updateControlsGeometry() {
updateInnerVisibleArea();
}
_composeControls->move(0, bottom - controlsHeight);
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
updateScrollDownPosition();
}
@ -1598,12 +1623,7 @@ void RepliesWidget::listCancelRequest() {
if (_inner && !_inner->getSelectedItems().empty()) {
clearSelected();
return;
}
if (_composeControls->isEditingMessage()) {
_composeControls->cancelEditMessage();
return;
} else if (_composeControls->replyingToMessage()) {
_composeControls->cancelReplyMessage();
} else if (_composeControls->handleCancelRequest()) {
return;
}
controller()->showBackFromStack();

View file

@ -186,6 +186,7 @@ private:
[[nodiscard]] MsgId replyToId() const;
[[nodiscard]] HistoryItem *lookupRoot() const;
[[nodiscard]] bool computeAreComments() const;
void orderWidgets();
void pushReplyReturn(not_null<HistoryItem*> item);
void computeCurrentReplyReturn();

View file

@ -180,6 +180,19 @@ void ScheduledWidget::setupComposeControls() {
sendVoice(data.bytes, data.waveform, data.duration);
}, lifetime());
_composeControls->sendCommandRequests(
) | rpl::start_with_next([=](const QString &command) {
const auto callback = [=](Api::SendOptions options) {
auto message = ApiWrap::MessageToSend(_history);
message.textWithTags = { command };
message.action.options = options;
session().api().sendMessage(std::move(message));
};
Ui::show(
PrepareScheduleBox(this, sendMenuType(), callback),
Ui::LayerOption::KeepOther);
}, lifetime());
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
_composeControls->editRequests(
) | rpl::start_with_next([=](auto data) {
@ -979,6 +992,7 @@ void ScheduledWidget::updateControlsGeometry() {
updateInnerVisibleArea();
}
_composeControls->move(0, bottom - controlsHeight);
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
updateScrollDownPosition();
}
@ -1057,9 +1071,7 @@ void ScheduledWidget::listCancelRequest() {
if (_inner && !_inner->getSelectedItems().empty()) {
clearSelected();
return;
}
if (_composeControls->isEditingMessage()) {
_composeControls->cancelEditMessage();
} else if (_composeControls->handleCancelRequest()) {
return;
}
controller()->showBackFromStack();

View file

@ -35,7 +35,7 @@ struct Contact {
QString lastName;
};
class Autocomplete : public Ui::RpWidget {
class Autocomplete final : public Ui::RpWidget {
public:
Autocomplete(QWidget *parent, not_null<Main::Session*> session);