2017-07-19 18:37:49 +00:00
|
|
|
/*
|
|
|
|
This file is part of Telegram Desktop,
|
2018-01-03 10:23:14 +00:00
|
|
|
the official desktop application for the Telegram messaging service.
|
2017-07-19 18:37:49 +00:00
|
|
|
|
2018-01-03 10:23:14 +00:00
|
|
|
For license and copyright information please follow this link:
|
|
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
2017-07-19 18:37:49 +00:00
|
|
|
*/
|
|
|
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
|
|
|
|
2019-03-28 08:48:32 +00:00
|
|
|
#include "chat_helpers/emoji_keywords.h"
|
2017-07-25 18:16:37 +00:00
|
|
|
#include "chat_helpers/emoji_suggestions_helper.h"
|
2017-07-22 08:35:18 +00:00
|
|
|
#include "ui/effects/ripple_animation.h"
|
|
|
|
#include "ui/widgets/shadow.h"
|
2018-10-13 17:35:30 +00:00
|
|
|
#include "ui/widgets/inner_dropdown.h"
|
2018-11-21 10:09:46 +00:00
|
|
|
#include "ui/widgets/input_fields.h"
|
2018-10-13 17:35:30 +00:00
|
|
|
#include "ui/emoji_config.h"
|
2017-07-22 08:35:18 +00:00
|
|
|
#include "platform/platform_specific.h"
|
2019-03-28 08:48:32 +00:00
|
|
|
#include "core/application.h"
|
2018-11-21 10:09:46 +00:00
|
|
|
#include "core/event_filter.h"
|
2017-07-22 08:35:18 +00:00
|
|
|
#include "styles/style_chat_helpers.h"
|
|
|
|
|
|
|
|
namespace Ui {
|
|
|
|
namespace Emoji {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
constexpr auto kRowLimit = 5;
|
2017-07-24 16:18:39 +00:00
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
class SuggestionsWidget::Row {
|
|
|
|
public:
|
2017-08-17 08:31:24 +00:00
|
|
|
Row(not_null<EmojiPtr> emoji, const QString &label, const QString &replacement);
|
2017-07-22 08:35:18 +00:00
|
|
|
Row(const Row &other) = delete;
|
|
|
|
Row &operator=(const Row &other) = delete;
|
|
|
|
Row(Row &&other) = default;
|
|
|
|
Row &operator=(Row &&other) = default;
|
|
|
|
~Row();
|
|
|
|
|
2017-08-17 08:31:24 +00:00
|
|
|
not_null<EmojiPtr> emoji() const {
|
2017-07-22 08:35:18 +00:00
|
|
|
return _emoji;
|
|
|
|
}
|
|
|
|
const QString &label() const {
|
|
|
|
return _label;
|
|
|
|
}
|
|
|
|
const QString &replacement() const {
|
|
|
|
return _replacement;
|
|
|
|
}
|
|
|
|
RippleAnimation *ripple() const {
|
|
|
|
return _ripple.get();
|
|
|
|
}
|
|
|
|
void setRipple(std::unique_ptr<RippleAnimation> ripple) {
|
|
|
|
_ripple = std::move(ripple);
|
|
|
|
}
|
|
|
|
void resetRipple() {
|
|
|
|
_ripple.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2017-08-17 08:31:24 +00:00
|
|
|
not_null<EmojiPtr> _emoji;
|
2017-07-22 08:35:18 +00:00
|
|
|
QString _label;
|
|
|
|
QString _replacement;
|
|
|
|
std::unique_ptr<RippleAnimation> _ripple;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2017-08-17 08:31:24 +00:00
|
|
|
SuggestionsWidget::Row::Row(not_null<EmojiPtr> emoji, const QString &label, const QString &replacement)
|
2017-07-22 08:35:18 +00:00
|
|
|
: _emoji(emoji)
|
|
|
|
, _label(label)
|
|
|
|
, _replacement(replacement) {
|
|
|
|
}
|
|
|
|
|
|
|
|
SuggestionsWidget::Row::~Row() = default;
|
|
|
|
|
|
|
|
SuggestionsWidget::SuggestionsWidget(QWidget *parent, const style::Menu &st) : TWidget(parent)
|
|
|
|
, _st(&st)
|
|
|
|
, _rowHeight(_st->itemPadding.top() + _st->itemFont->height + _st->itemPadding.bottom()) {
|
|
|
|
setMouseTracking(true);
|
|
|
|
}
|
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
rpl::producer<bool> SuggestionsWidget::toggleAnimated() const {
|
|
|
|
return _toggleAnimated.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<QString> SuggestionsWidget::triggered() const {
|
|
|
|
return _triggered.events();
|
|
|
|
}
|
|
|
|
|
2019-03-29 16:04:05 +00:00
|
|
|
void SuggestionsWidget::showWithQuery(const QString &query, bool force) {
|
|
|
|
if (!force && (_query == query)) {
|
2017-07-22 08:35:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_query = query;
|
2017-07-25 18:16:37 +00:00
|
|
|
auto rows = getRowsByQuery();
|
2017-07-22 08:35:18 +00:00
|
|
|
if (rows.empty()) {
|
2018-11-21 10:09:46 +00:00
|
|
|
_toggleAnimated.fire(false);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
clearSelection();
|
|
|
|
_rows = std::move(rows);
|
|
|
|
resizeToRows();
|
|
|
|
update();
|
|
|
|
if (!_rows.empty()) {
|
|
|
|
setSelected(0);
|
|
|
|
}
|
|
|
|
if (!_rows.empty()) {
|
2018-11-21 10:09:46 +00:00
|
|
|
_toggleAnimated.fire(true);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-25 18:16:37 +00:00
|
|
|
std::vector<SuggestionsWidget::Row> SuggestionsWidget::getRowsByQuery() const {
|
|
|
|
auto result = std::vector<Row>();
|
|
|
|
if (_query.isEmpty()) {
|
|
|
|
return result;
|
|
|
|
}
|
2019-03-28 08:48:32 +00:00
|
|
|
auto suggestions = std::vector<Row>();
|
2019-03-29 13:51:18 +00:00
|
|
|
const auto middle = (_query[0] == ':');
|
|
|
|
const auto real = middle ? _query.mid(1) : _query;
|
|
|
|
const auto simple = [&] {
|
|
|
|
if (!middle || _query.size() > 2) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Suggest :D and :-P only as exact matches.
|
|
|
|
return ranges::find_if(_query, [](QChar ch) { return ch.isLower(); })
|
|
|
|
== _query.end();
|
|
|
|
}();
|
|
|
|
const auto exact = !middle || simple;
|
|
|
|
const auto results = Core::App().emojiKeywords().query(real, exact);
|
2019-03-28 08:48:32 +00:00
|
|
|
for (const auto &result : results) {
|
|
|
|
suggestions.emplace_back(
|
|
|
|
result.emoji,
|
|
|
|
result.label,
|
|
|
|
result.replacement);
|
|
|
|
}
|
2017-07-25 18:16:37 +00:00
|
|
|
if (suggestions.empty()) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
auto recents = 0;
|
2019-03-28 08:48:32 +00:00
|
|
|
const auto &recent = GetRecent();
|
|
|
|
for (const auto &item : recent) {
|
|
|
|
const auto emoji = item.first->original()
|
|
|
|
? item.first->original()
|
|
|
|
: item.first;
|
|
|
|
const auto it = ranges::find(suggestions, emoji, [](const Row &row) {
|
|
|
|
return row.emoji().get();
|
|
|
|
});
|
|
|
|
if (it == end(suggestions)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const auto index = (it - begin(suggestions));
|
|
|
|
if (index < recents) {
|
|
|
|
continue;
|
|
|
|
} else if (index > recents) {
|
|
|
|
auto recentSuggestion = std::move(suggestions[index]);
|
|
|
|
for (auto i = index; i != recents; --i) {
|
|
|
|
suggestions[i] = std::move(suggestions[i - 1]);
|
2017-07-25 18:16:37 +00:00
|
|
|
}
|
2019-03-28 08:48:32 +00:00
|
|
|
suggestions[recents] = std::move(recentSuggestion);
|
2017-07-25 18:16:37 +00:00
|
|
|
}
|
2019-03-28 08:48:32 +00:00
|
|
|
++recents;
|
2017-07-25 18:16:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result.reserve(kRowLimit);
|
|
|
|
auto index = 0;
|
2019-03-28 08:48:32 +00:00
|
|
|
for (const auto &item : suggestions) {
|
|
|
|
const auto emoji = [&] {
|
|
|
|
const auto result = item.emoji();
|
|
|
|
const auto &variants = cEmojiVariants();
|
|
|
|
const auto i = result->hasVariants()
|
|
|
|
? variants.constFind(result->nonColoredId())
|
|
|
|
: variants.cend();
|
|
|
|
return (i != variants.cend())
|
|
|
|
? result->variant(i.value())
|
|
|
|
: result.get();
|
|
|
|
}();
|
|
|
|
result.emplace_back(emoji, item.label(), item.replacement());
|
|
|
|
if (result.size() == kRowLimit) {
|
|
|
|
break;
|
2017-07-25 18:16:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
void SuggestionsWidget::resizeToRows() {
|
|
|
|
auto newWidth = 0;
|
|
|
|
for (auto &row : _rows) {
|
|
|
|
accumulate_max(newWidth, countWidth(row));
|
|
|
|
}
|
|
|
|
newWidth = snap(newWidth, _st->widthMin, _st->widthMax);
|
|
|
|
auto newHeight = _st->skip + (_rows.size() * _rowHeight) + _st->skip;
|
|
|
|
resize(newWidth, newHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
int SuggestionsWidget::countWidth(const Row &row) {
|
|
|
|
auto textw = _st->itemFont->width(row.label());
|
|
|
|
return _st->itemPadding.left() + textw + _st->itemPadding.right();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::paintEvent(QPaintEvent *e) {
|
|
|
|
Painter p(this);
|
|
|
|
|
2019-02-19 06:57:53 +00:00
|
|
|
auto ms = crl::now();
|
2017-07-22 08:35:18 +00:00
|
|
|
auto clip = e->rect();
|
|
|
|
|
|
|
|
auto topskip = QRect(0, 0, width(), _st->skip);
|
|
|
|
auto bottomskip = QRect(0, height() - _st->skip, width(), _st->skip);
|
|
|
|
if (clip.intersects(topskip)) p.fillRect(clip.intersected(topskip), _st->itemBg);
|
|
|
|
if (clip.intersects(bottomskip)) p.fillRect(clip.intersected(bottomskip), _st->itemBg);
|
|
|
|
|
2018-10-13 17:35:30 +00:00
|
|
|
const auto top = _st->skip;
|
2017-07-22 08:35:18 +00:00
|
|
|
p.setFont(_st->itemFont);
|
2018-10-13 17:35:30 +00:00
|
|
|
const auto from = floorclamp(clip.top() - top, _rowHeight, 0, _rows.size());
|
|
|
|
const auto to = ceilclamp(clip.top() + clip.height() - top, _rowHeight, 0, _rows.size());
|
2017-07-22 08:35:18 +00:00
|
|
|
p.translate(0, top + from * _rowHeight);
|
|
|
|
for (auto i = from; i != to; ++i) {
|
|
|
|
auto &row = _rows[i];
|
2018-10-13 17:35:30 +00:00
|
|
|
const auto selected = (i == _selected || i == _pressed);
|
2017-07-22 08:35:18 +00:00
|
|
|
p.fillRect(0, 0, width(), _rowHeight, selected ? _st->itemBgOver : _st->itemBg);
|
2018-10-13 17:35:30 +00:00
|
|
|
if (const auto ripple = row.ripple()) {
|
2017-07-22 08:35:18 +00:00
|
|
|
ripple->paint(p, 0, 0, width(), ms);
|
|
|
|
if (ripple->empty()) {
|
|
|
|
row.resetRipple();
|
|
|
|
}
|
|
|
|
}
|
2018-10-13 17:35:30 +00:00
|
|
|
const auto emoji = row.emoji();
|
|
|
|
const auto esize = Ui::Emoji::GetSizeLarge();
|
|
|
|
Ui::Emoji::Draw(
|
|
|
|
p,
|
|
|
|
emoji,
|
|
|
|
esize,
|
|
|
|
(_st->itemPadding.left() - (esize / cIntRetinaFactor())) / 2,
|
|
|
|
(_rowHeight - (esize / cIntRetinaFactor())) / 2);
|
2017-07-22 08:35:18 +00:00
|
|
|
p.setPen(selected ? _st->itemFgOver : _st->itemFg);
|
|
|
|
p.drawTextLeft(_st->itemPadding.left(), _st->itemPadding.top(), width(), row.label());
|
|
|
|
p.translate(0, _rowHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::keyPressEvent(QKeyEvent *e) {
|
|
|
|
handleKeyEvent(e->key());
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::handleKeyEvent(int key) {
|
|
|
|
if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Tab) {
|
2017-07-27 10:22:51 +00:00
|
|
|
triggerSelectedRow();
|
2017-07-22 08:35:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ((key != Qt::Key_Up && key != Qt::Key_Down) || _rows.size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto delta = (key == Qt::Key_Down ? 1 : -1), start = _selected;
|
|
|
|
if (start < 0 || start >= _rows.size()) {
|
|
|
|
start = (delta > 0) ? (_rows.size() - 1) : 0;
|
|
|
|
}
|
|
|
|
auto newSelected = start + delta;
|
|
|
|
if (newSelected < 0) {
|
|
|
|
newSelected += _rows.size();
|
|
|
|
} else if (newSelected >= _rows.size()) {
|
|
|
|
newSelected -= _rows.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
_mouseSelection = false;
|
2018-12-25 12:41:40 +00:00
|
|
|
_lastMousePosition = std::nullopt;
|
2017-07-22 08:35:18 +00:00
|
|
|
setSelected(newSelected);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::setSelected(int selected) {
|
|
|
|
if (selected >= _rows.size()) {
|
|
|
|
selected = -1;
|
|
|
|
}
|
|
|
|
if (_selected != selected) {
|
|
|
|
updateSelectedItem();
|
|
|
|
_selected = selected;
|
|
|
|
updateSelectedItem();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::setPressed(int pressed) {
|
|
|
|
if (pressed >= _rows.size()) {
|
|
|
|
pressed = -1;
|
|
|
|
}
|
|
|
|
if (_pressed != pressed) {
|
|
|
|
_pressed = pressed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::clearMouseSelection() {
|
|
|
|
if (_mouseSelection) {
|
|
|
|
clearSelection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::clearSelection() {
|
|
|
|
_mouseSelection = false;
|
2018-12-25 12:41:40 +00:00
|
|
|
_lastMousePosition = std::nullopt;
|
2017-07-22 08:35:18 +00:00
|
|
|
setSelected(-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
int SuggestionsWidget::itemTop(int index) {
|
|
|
|
if (index > _rows.size()) {
|
|
|
|
index = _rows.size();
|
|
|
|
}
|
|
|
|
return _st->skip + (_rowHeight * index);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::updateItem(int index) {
|
|
|
|
if (index >= 0 && index < _rows.size()) {
|
|
|
|
update(0, itemTop(index), width(), _rowHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::updateSelectedItem() {
|
|
|
|
updateItem(_selected);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) {
|
|
|
|
auto inner = rect().marginsRemoved(QMargins(0, _st->skip, 0, _st->skip));
|
|
|
|
auto localPosition = e->pos();
|
|
|
|
if (inner.contains(localPosition)) {
|
2018-12-25 12:41:40 +00:00
|
|
|
const auto globalPosition = e->globalPos();
|
|
|
|
if (!_lastMousePosition) {
|
|
|
|
_lastMousePosition = globalPosition;
|
|
|
|
return;
|
|
|
|
} else if (!_mouseSelection
|
|
|
|
&& *_lastMousePosition == globalPosition) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
selectByMouse(globalPosition);
|
2017-07-22 08:35:18 +00:00
|
|
|
} else {
|
|
|
|
clearMouseSelection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-25 12:41:40 +00:00
|
|
|
void SuggestionsWidget::selectByMouse(QPoint globalPosition) {
|
|
|
|
_mouseSelection = true;
|
|
|
|
_lastMousePosition = globalPosition;
|
2017-07-22 08:35:18 +00:00
|
|
|
auto p = mapFromGlobal(globalPosition) - QPoint(0, _st->skip);
|
|
|
|
auto selected = (p.y() >= 0) ? (p.y() / _rowHeight) : -1;
|
|
|
|
setSelected((selected >= 0 && selected < _rows.size()) ? selected : -1);
|
|
|
|
}
|
2018-12-25 12:41:40 +00:00
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
void SuggestionsWidget::mousePressEvent(QMouseEvent *e) {
|
2018-12-25 12:41:40 +00:00
|
|
|
selectByMouse(e->globalPos());
|
2017-07-22 08:35:18 +00:00
|
|
|
if (_selected >= 0 && _selected < _rows.size()) {
|
|
|
|
setPressed(_selected);
|
|
|
|
if (!_rows[_pressed].ripple()) {
|
|
|
|
auto mask = RippleAnimation::rectMask(QSize(width(), _rowHeight));
|
|
|
|
_rows[_pressed].setRipple(std::make_unique<RippleAnimation>(_st->ripple, std::move(mask), [this, selected = _pressed] {
|
|
|
|
updateItem(selected);
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
_rows[_pressed].ripple()->add(mapFromGlobal(QCursor::pos()) - QPoint(0, itemTop(_pressed)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::mouseReleaseEvent(QMouseEvent *e) {
|
|
|
|
if (_pressed >= 0 && _pressed < _rows.size()) {
|
|
|
|
auto pressed = _pressed;
|
|
|
|
setPressed(-1);
|
|
|
|
if (_rows[pressed].ripple()) {
|
|
|
|
_rows[pressed].ripple()->lastStop();
|
|
|
|
}
|
|
|
|
if (pressed == _selected) {
|
2017-07-27 10:22:51 +00:00
|
|
|
triggerRow(_rows[_selected]);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-27 10:22:51 +00:00
|
|
|
void SuggestionsWidget::triggerSelectedRow() {
|
|
|
|
if (_selected >= 0 && _selected < _rows.size()) {
|
|
|
|
triggerRow(_rows[_selected]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::triggerRow(const Row &row) {
|
2018-11-21 10:09:46 +00:00
|
|
|
_triggered.fire(row.emoji()->text());
|
2017-07-27 10:22:51 +00:00
|
|
|
}
|
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
void SuggestionsWidget::enterEventHook(QEvent *e) {
|
|
|
|
auto mouse = QCursor::pos();
|
|
|
|
if (!rect().marginsRemoved(QMargins(0, _st->skip, 0, _st->skip)).contains(mapFromGlobal(mouse))) {
|
|
|
|
clearMouseSelection();
|
|
|
|
}
|
|
|
|
return TWidget::enterEventHook(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsWidget::leaveEventHook(QEvent *e) {
|
|
|
|
clearMouseSelection();
|
|
|
|
return TWidget::leaveEventHook(e);
|
|
|
|
}
|
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
SuggestionsController::SuggestionsController(
|
|
|
|
not_null<QWidget*> outer,
|
|
|
|
not_null<QTextEdit*> field)
|
|
|
|
: _field(field) {
|
|
|
|
_container = base::make_unique_q<InnerDropdown>(
|
|
|
|
outer,
|
|
|
|
st::emojiSuggestionsDropdown);
|
2017-07-22 08:35:18 +00:00
|
|
|
_container->setAutoHiding(false);
|
2018-11-21 10:09:46 +00:00
|
|
|
_suggestions = _container->setOwnedWidget(
|
|
|
|
object_ptr<Ui::Emoji::SuggestionsWidget>(
|
|
|
|
_container,
|
|
|
|
st::emojiSuggestionsMenu));
|
2017-07-22 08:35:18 +00:00
|
|
|
|
2018-05-13 15:56:08 +00:00
|
|
|
setReplaceCallback(nullptr);
|
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
_fieldFilter.reset(Core::InstallEventFilter(
|
|
|
|
_field,
|
|
|
|
[=](not_null<QEvent*> event) { return fieldFilter(event); }));
|
|
|
|
_outerFilter.reset(Core::InstallEventFilter(
|
|
|
|
outer,
|
|
|
|
[=](not_null<QEvent*> event) { return outerFilter(event); }));
|
|
|
|
QObject::connect(
|
|
|
|
_field,
|
|
|
|
&QTextEdit::textChanged,
|
|
|
|
_container,
|
|
|
|
[=] { handleTextChange(); });
|
|
|
|
QObject::connect(
|
|
|
|
_field,
|
|
|
|
&QTextEdit::cursorPositionChanged,
|
|
|
|
_container,
|
|
|
|
[=] { handleCursorPositionChange(); });
|
|
|
|
|
|
|
|
_suggestions->toggleAnimated(
|
|
|
|
) | rpl::start_with_next([=](bool visible) {
|
|
|
|
suggestionsUpdated(visible);
|
|
|
|
}, _lifetime);
|
|
|
|
_suggestions->triggered(
|
|
|
|
) | rpl::start_with_next([=](QString replacement) {
|
|
|
|
replaceCurrent(replacement);
|
|
|
|
}, _lifetime);
|
2019-03-29 16:04:05 +00:00
|
|
|
Core::App().emojiKeywords().refreshed(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
showFromTextChange(getEmojiQuery(), true);
|
|
|
|
}, _lifetime);
|
2017-07-22 08:35:18 +00:00
|
|
|
|
|
|
|
updateForceHidden();
|
|
|
|
|
|
|
|
handleTextChange();
|
|
|
|
}
|
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
SuggestionsController *SuggestionsController::Init(
|
|
|
|
not_null<QWidget*> outer,
|
|
|
|
not_null<Ui::InputField*> field) {
|
|
|
|
const auto result = Ui::CreateChild<SuggestionsController>(
|
|
|
|
field.get(),
|
|
|
|
outer,
|
|
|
|
field->rawTextEdit());
|
|
|
|
result->setReplaceCallback([=](
|
|
|
|
int from,
|
|
|
|
int till,
|
|
|
|
const QString &replacement) {
|
|
|
|
field->commitInstantReplacement(from, till, replacement);
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-05-13 15:56:08 +00:00
|
|
|
void SuggestionsController::setReplaceCallback(
|
2018-06-04 15:35:11 +00:00
|
|
|
Fn<void(
|
2018-05-13 15:56:08 +00:00
|
|
|
int from,
|
|
|
|
int till,
|
|
|
|
const QString &replacement)> callback) {
|
|
|
|
if (callback) {
|
|
|
|
_replaceCallback = std::move(callback);
|
|
|
|
} else {
|
|
|
|
_replaceCallback = [=](int from, int till, const QString &replacement) {
|
|
|
|
auto cursor = _field->textCursor();
|
|
|
|
cursor.setPosition(from);
|
|
|
|
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
|
|
cursor.insertText(replacement);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
void SuggestionsController::handleTextChange() {
|
2019-03-28 08:48:32 +00:00
|
|
|
if (Global::SuggestEmoji() && _field->textCursor().position() > 0) {
|
|
|
|
Core::App().emojiKeywords().refresh();
|
|
|
|
}
|
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
_ignoreCursorPositionChange = true;
|
2018-11-21 10:09:46 +00:00
|
|
|
InvokeQueued(_container, [=] { _ignoreCursorPositionChange = false; });
|
2017-07-22 08:35:18 +00:00
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
const auto query = getEmojiQuery();
|
2017-07-22 08:35:18 +00:00
|
|
|
if (query.isEmpty() || _textChangeAfterKeyPress) {
|
2019-03-29 16:04:05 +00:00
|
|
|
showFromTextChange(query);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-29 16:04:05 +00:00
|
|
|
void SuggestionsController::showFromTextChange(
|
|
|
|
const QString &query,
|
|
|
|
bool force) {
|
|
|
|
_suggestions->showWithQuery(query, force);
|
|
|
|
}
|
|
|
|
|
2017-07-22 08:35:18 +00:00
|
|
|
QString SuggestionsController::getEmojiQuery() {
|
2018-05-13 15:56:08 +00:00
|
|
|
if (!Global::SuggestEmoji()) {
|
2017-07-25 18:16:37 +00:00
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
|
2019-03-29 12:21:11 +00:00
|
|
|
const auto cursor = _field->textCursor();
|
2018-05-26 14:58:21 +00:00
|
|
|
if (cursor.hasSelection()) {
|
2017-07-22 08:35:18 +00:00
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
|
2019-03-29 13:51:18 +00:00
|
|
|
const auto modernLimit = Core::App().emojiKeywords().maxQueryLength();
|
|
|
|
const auto legacyLimit = GetSuggestionMaxLength();
|
2019-03-29 12:21:11 +00:00
|
|
|
const auto position = cursor.position();
|
|
|
|
const auto findTextPart = [&] {
|
2017-07-22 08:35:18 +00:00
|
|
|
auto document = _field->document();
|
|
|
|
auto block = document->findBlock(position);
|
|
|
|
for (auto i = block.begin(); !i.atEnd(); ++i) {
|
|
|
|
auto fragment = i.fragment();
|
|
|
|
if (!fragment.isValid()) continue;
|
|
|
|
|
|
|
|
auto from = fragment.position();
|
|
|
|
auto till = from + fragment.length();
|
|
|
|
if (from >= position || till < position) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (fragment.charFormat().isImageFormat()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
_queryStartPosition = from;
|
2018-03-14 23:10:03 +00:00
|
|
|
return fragment.text();
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
return QString();
|
|
|
|
};
|
|
|
|
|
2019-03-29 12:21:11 +00:00
|
|
|
const auto text = findTextPart();
|
2017-07-22 08:35:18 +00:00
|
|
|
if (text.isEmpty()) {
|
|
|
|
return QString();
|
|
|
|
}
|
2019-03-29 13:51:18 +00:00
|
|
|
const auto length = position - _queryStartPosition;
|
|
|
|
for (auto i = length; i != 0;) {
|
2019-03-29 12:21:11 +00:00
|
|
|
if (text[--i] == ':') {
|
2019-03-29 14:23:21 +00:00
|
|
|
const auto previous = (i > 0) ? text[i - 1] : QChar(0);
|
|
|
|
if (i > 0 && (previous.isLetter() || previous.isDigit())) {
|
|
|
|
return QString();
|
|
|
|
} else if (i + 1 == length || text[i + 1].isSpace()) {
|
2019-03-29 13:51:18 +00:00
|
|
|
return QString();
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
2019-03-29 13:51:18 +00:00
|
|
|
_queryStartPosition += i + 2;
|
|
|
|
return text.mid(i, length - i);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
2019-03-29 13:51:18 +00:00
|
|
|
if (length - i > legacyLimit && length - i > modernLimit) {
|
2017-07-22 08:35:18 +00:00
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
}
|
2019-03-29 13:51:18 +00:00
|
|
|
|
|
|
|
// Exact query should be full input field value.
|
|
|
|
const auto end = [&] {
|
|
|
|
auto cursor = _field->textCursor();
|
|
|
|
cursor.movePosition(QTextCursor::End);
|
|
|
|
return cursor.position();
|
|
|
|
}();
|
2019-03-29 14:23:21 +00:00
|
|
|
if (!length
|
|
|
|
|| text[0].isSpace()
|
|
|
|
|| (length > modernLimit)
|
2019-03-29 13:51:18 +00:00
|
|
|
|| (_queryStartPosition != 0)
|
|
|
|
|| (position != end)) {
|
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
return text.mid(0, length);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::replaceCurrent(const QString &replacement) {
|
2019-03-29 12:21:11 +00:00
|
|
|
const auto suggestion = getEmojiQuery();
|
2017-07-22 08:35:18 +00:00
|
|
|
if (suggestion.isEmpty()) {
|
|
|
|
_suggestions->showWithQuery(QString());
|
|
|
|
} else {
|
2018-05-13 15:56:08 +00:00
|
|
|
const auto cursor = _field->textCursor();
|
|
|
|
const auto position = cursor.position();
|
|
|
|
const auto from = position - suggestion.size();
|
|
|
|
_replaceCallback(from, position, replacement);
|
2017-07-25 18:16:37 +00:00
|
|
|
}
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::handleCursorPositionChange() {
|
2018-11-21 10:09:46 +00:00
|
|
|
InvokeQueued(_container, [=] {
|
2017-07-22 08:35:18 +00:00
|
|
|
if (_ignoreCursorPositionChange) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_suggestions->showWithQuery(QString());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::suggestionsUpdated(bool visible) {
|
|
|
|
_shown = visible;
|
|
|
|
if (_shown) {
|
|
|
|
_container->resizeToContent();
|
|
|
|
updateGeometry();
|
|
|
|
if (!_forceHidden) {
|
2018-11-21 10:09:46 +00:00
|
|
|
if (_container->isHidden() || _container->isHiding()) {
|
|
|
|
raise();
|
|
|
|
}
|
|
|
|
_container->showAnimated(
|
|
|
|
Ui::PanelAnimation::Origin::BottomLeft);
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
} else if (!_forceHidden) {
|
|
|
|
_container->hideAnimated();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::updateGeometry() {
|
|
|
|
auto cursor = _field->textCursor();
|
|
|
|
cursor.setPosition(_queryStartPosition);
|
|
|
|
auto aroundRect = _field->cursorRect(cursor);
|
|
|
|
aroundRect.setTopLeft(_field->viewport()->mapToGlobal(aroundRect.topLeft()));
|
|
|
|
aroundRect.setTopLeft(_container->parentWidget()->mapFromGlobal(aroundRect.topLeft()));
|
|
|
|
auto boundingRect = _container->parentWidget()->rect();
|
|
|
|
auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft;
|
|
|
|
auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft();
|
|
|
|
point -= rtl() ? QPoint(_container->width() - st::emojiSuggestionsDropdown.padding.right(), _container->height()) : QPoint(st::emojiSuggestionsDropdown.padding.left(), _container->height());
|
|
|
|
if (rtl()) {
|
|
|
|
if (point.x() < boundingRect.x()) {
|
|
|
|
point.setX(boundingRect.x());
|
|
|
|
}
|
|
|
|
if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {
|
|
|
|
point.setX(boundingRect.x() + boundingRect.width() - _container->width());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (point.x() + _container->width() > boundingRect.x() + boundingRect.width()) {
|
|
|
|
point.setX(boundingRect.x() + boundingRect.width() - _container->width());
|
|
|
|
}
|
|
|
|
if (point.x() < boundingRect.x()) {
|
|
|
|
point.setX(boundingRect.x());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (point.y() < boundingRect.y()) {
|
|
|
|
point.setY(aroundRect.y() + aroundRect.height());
|
|
|
|
origin = (origin == PanelAnimation::Origin::BottomRight) ? PanelAnimation::Origin::TopRight : PanelAnimation::Origin::TopLeft;
|
|
|
|
}
|
|
|
|
_container->move(point);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::updateForceHidden() {
|
2018-11-21 10:09:46 +00:00
|
|
|
_forceHidden = !_field->isVisible() || !_field->hasFocus();
|
2017-07-22 08:35:18 +00:00
|
|
|
if (_forceHidden) {
|
|
|
|
_container->hideFast();
|
|
|
|
} else if (_shown) {
|
|
|
|
_container->showFast();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
bool SuggestionsController::fieldFilter(not_null<QEvent*> event) {
|
|
|
|
auto type = event->type();
|
|
|
|
switch (type) {
|
|
|
|
case QEvent::Move:
|
|
|
|
case QEvent::Resize: {
|
|
|
|
if (_shown) {
|
|
|
|
updateGeometry();
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
|
|
|
|
case QEvent::Show:
|
|
|
|
case QEvent::ShowToParent:
|
|
|
|
case QEvent::Hide:
|
|
|
|
case QEvent::HideToParent:
|
|
|
|
case QEvent::FocusIn:
|
|
|
|
case QEvent::FocusOut: {
|
|
|
|
updateForceHidden();
|
|
|
|
} break;
|
|
|
|
|
|
|
|
case QEvent::KeyPress: {
|
|
|
|
const auto key = static_cast<QKeyEvent*>(event.get())->key();
|
|
|
|
switch (key) {
|
|
|
|
case Qt::Key_Enter:
|
|
|
|
case Qt::Key_Return:
|
|
|
|
case Qt::Key_Tab:
|
|
|
|
case Qt::Key_Up:
|
|
|
|
case Qt::Key_Down:
|
|
|
|
if (_shown && !_forceHidden) {
|
|
|
|
_suggestions->handleKeyEvent(key);
|
|
|
|
return true;
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
2018-11-21 10:09:46 +00:00
|
|
|
break;
|
2017-07-22 08:35:18 +00:00
|
|
|
|
2018-11-21 10:09:46 +00:00
|
|
|
case Qt::Key_Escape:
|
|
|
|
if (_shown && !_forceHidden) {
|
|
|
|
_suggestions->showWithQuery(QString());
|
|
|
|
return true;
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
2018-11-21 10:09:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
_textChangeAfterKeyPress = true;
|
|
|
|
InvokeQueued(_container, [=] { _textChangeAfterKeyPress = false; });
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SuggestionsController::outerFilter(not_null<QEvent*> event) {
|
|
|
|
auto type = event->type();
|
|
|
|
switch (type) {
|
|
|
|
case QEvent::Move:
|
|
|
|
case QEvent::Resize: {
|
2018-11-22 12:48:50 +00:00
|
|
|
// updateGeometry uses not only container geometry, but also
|
|
|
|
// container children geometries that will be updated later.
|
|
|
|
InvokeQueued(_container, [=] {
|
|
|
|
if (_shown) {
|
|
|
|
updateGeometry();
|
|
|
|
}
|
|
|
|
});
|
2018-11-21 10:09:46 +00:00
|
|
|
} break;
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
2018-11-21 10:09:46 +00:00
|
|
|
return false;
|
2017-07-22 08:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SuggestionsController::raise() {
|
|
|
|
_container->raise();
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Emoji
|
|
|
|
} // namespace Ui
|