mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-04-01 00:08:02 +00:00
Add emoji autocomplete to history message field.
This commit is contained in:
parent
8f8100af3a
commit
2dec9c46a7
@ -222,3 +222,19 @@ gifsSearchCancel: contactsSearchCancel;
|
||||
gifsSearchCancelPosition: point(1px, 1px);
|
||||
gifsSearchIcon: boxFieldSearchIcon;
|
||||
gifsSearchIconPosition: point(6px, 7px);
|
||||
|
||||
emojiSuggestionsDropdown: InnerDropdown(defaultInnerDropdown) {
|
||||
scroll: ScrollArea(defaultSolidScroll) {
|
||||
deltat: 0px;
|
||||
deltab: 0px;
|
||||
round: 1px;
|
||||
width: 8px;
|
||||
deltax: 3px;
|
||||
}
|
||||
scrollMargin: margins(0px, 5px, 0px, 5px);
|
||||
scrollPadding: margins(0px, 3px, 0px, 3px);
|
||||
}
|
||||
emojiSuggestionsMenu: Menu(defaultMenu) {
|
||||
itemPadding: margins(48px, 8px, 17px, 7px);
|
||||
widthMax: 512px;
|
||||
}
|
||||
|
@ -20,3 +20,507 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "ui/widgets/inner_dropdown.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace Emoji {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRowLimit = 5;
|
||||
constexpr auto kLargestReplacementLength = 128;
|
||||
|
||||
} // namespace
|
||||
|
||||
class SuggestionsWidget::Row {
|
||||
public:
|
||||
Row(gsl::not_null<EmojiPtr> emoji, const QString &label, const QString &replacement);
|
||||
Row(const Row &other) = delete;
|
||||
Row &operator=(const Row &other) = delete;
|
||||
Row(Row &&other) = default;
|
||||
Row &operator=(Row &&other) = default;
|
||||
~Row();
|
||||
|
||||
gsl::not_null<EmojiPtr> emoji() const {
|
||||
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:
|
||||
gsl::not_null<EmojiPtr> _emoji;
|
||||
QString _label;
|
||||
QString _replacement;
|
||||
std::unique_ptr<RippleAnimation> _ripple;
|
||||
|
||||
};
|
||||
|
||||
SuggestionsWidget::Row::Row(gsl::not_null<EmojiPtr> emoji, const QString &label, const QString &replacement)
|
||||
: _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);
|
||||
}
|
||||
|
||||
void SuggestionsWidget::showWithQuery(const QString &query) {
|
||||
if (_query == query) {
|
||||
return;
|
||||
}
|
||||
auto rows = std::vector<Row>();
|
||||
_query = query;
|
||||
if (!_query.isEmpty()) {
|
||||
rows.reserve(kRowLimit);
|
||||
for (auto &item : GetSuggestions(_query)) {
|
||||
if (auto emoji = Find(item.id)) {
|
||||
rows.emplace_back(emoji, item.label, item.replacement);
|
||||
if (rows.size() == kRowLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rows.empty()) {
|
||||
toggleAnimated.notify(false, true);
|
||||
}
|
||||
clearSelection();
|
||||
_rows = std::move(rows);
|
||||
resizeToRows();
|
||||
update();
|
||||
if (!_rows.empty()) {
|
||||
setSelected(0);
|
||||
}
|
||||
if (!_rows.empty()) {
|
||||
toggleAnimated.notify(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
auto ms = getms();
|
||||
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);
|
||||
|
||||
auto top = _st->skip;
|
||||
p.setFont(_st->itemFont);
|
||||
auto from = floorclamp(clip.top() - top, _rowHeight, 0, _rows.size());
|
||||
auto to = ceilclamp(clip.top() + clip.height() - top, _rowHeight, 0, _rows.size());
|
||||
p.translate(0, top + from * _rowHeight);
|
||||
for (auto i = from; i != to; ++i) {
|
||||
auto &row = _rows[i];
|
||||
auto selected = (i == _selected || i == _pressed);
|
||||
p.fillRect(0, 0, width(), _rowHeight, selected ? _st->itemBgOver : _st->itemBg);
|
||||
if (auto ripple = row.ripple()) {
|
||||
ripple->paint(p, 0, 0, width(), ms);
|
||||
if (ripple->empty()) {
|
||||
row.resetRipple();
|
||||
}
|
||||
}
|
||||
auto emoji = row.emoji();
|
||||
auto esize = Ui::Emoji::Size(Ui::Emoji::Index() + 1);
|
||||
p.drawPixmapLeft((_st->itemPadding.left() - (esize / cIntRetinaFactor())) / 2, (_rowHeight - (esize / cIntRetinaFactor())) / 2, width(), App::emojiLarge(), QRect(emoji->x() * esize, emoji->y() * esize, esize, esize));
|
||||
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) {
|
||||
if (_selected >= 0 && _selected < _rows.size()) {
|
||||
triggered.notify(_rows[_selected].replacement(), true);
|
||||
}
|
||||
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;
|
||||
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;
|
||||
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)) {
|
||||
_mouseSelection = true;
|
||||
updateSelection(e->globalPos());
|
||||
} else {
|
||||
clearMouseSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void SuggestionsWidget::updateSelection(QPoint globalPosition) {
|
||||
if (!_mouseSelection) return;
|
||||
|
||||
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);
|
||||
}
|
||||
void SuggestionsWidget::mousePressEvent(QMouseEvent *e) {
|
||||
if (!_mouseSelection) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
triggered.notify(_rows[_selected].replacement(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
SuggestionsController::SuggestionsController(QWidget *parent, gsl::not_null<QTextEdit*> field) : QObject(nullptr)
|
||||
, _field(field)
|
||||
, _container(parent, st::emojiSuggestionsDropdown)
|
||||
, _suggestions(_container->setOwnedWidget(object_ptr<Ui::Emoji::SuggestionsWidget>(parent, st::emojiSuggestionsMenu))) {
|
||||
_container->setAutoHiding(false);
|
||||
|
||||
_field->installEventFilter(this);
|
||||
connect(_field, &QTextEdit::textChanged, this, [this] { handleTextChange(); });
|
||||
connect(_field, &QTextEdit::cursorPositionChanged, this, [this] { handleCursorPositionChange(); });
|
||||
|
||||
subscribe(_suggestions->toggleAnimated, [this](bool visible) { suggestionsUpdated(visible); });
|
||||
subscribe(_suggestions->triggered, [this](QString replacement) { replaceCurrent(replacement); });
|
||||
updateForceHidden();
|
||||
|
||||
handleTextChange();
|
||||
}
|
||||
|
||||
void SuggestionsController::handleTextChange() {
|
||||
_ignoreCursorPositionChange = true;
|
||||
InvokeQueued(this, [this] { _ignoreCursorPositionChange = false; });
|
||||
|
||||
auto query = getEmojiQuery();
|
||||
if (query.isEmpty() || _textChangeAfterKeyPress) {
|
||||
_suggestions->showWithQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
QString SuggestionsController::getEmojiQuery() {
|
||||
auto cursor = _field->textCursor();
|
||||
auto position = _field->textCursor().position();
|
||||
if (cursor.anchor() != position) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
auto findTextPart = [this, &position] {
|
||||
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;
|
||||
}
|
||||
position -= from;
|
||||
_queryStartPosition = from;
|
||||
return fragment.text();
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
auto text = findTextPart();
|
||||
if (text.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
auto isSuggestionChar = [](QChar ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '_') || (ch == '-') || (ch == '+');
|
||||
};
|
||||
auto isGoodCharBeforeSuggestion = [isSuggestionChar](QChar ch) {
|
||||
return !isSuggestionChar(ch) || (ch == 0);
|
||||
};
|
||||
t_assert(position > 0 && position <= text.size());
|
||||
for (auto i = position; i != 0;) {
|
||||
auto ch = text[--i];
|
||||
if (ch == ':') {
|
||||
auto beforeColon = (i < 1) ? QChar(0) : text[i - 1];
|
||||
if (isGoodCharBeforeSuggestion(beforeColon)) {
|
||||
// At least one letter after colon.
|
||||
if (position > i + 1) {
|
||||
// Skip colon and the first letter.
|
||||
_queryStartPosition += i + 2;
|
||||
return text.mid(i, position - i);
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
if (position - i > kLargestReplacementLength) {
|
||||
return QString();
|
||||
}
|
||||
if (!isSuggestionChar(ch)) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void SuggestionsController::replaceCurrent(const QString &replacement) {
|
||||
auto cursor = _field->textCursor();
|
||||
auto suggestion = getEmojiQuery();
|
||||
if (suggestion.isEmpty()) {
|
||||
_suggestions->showWithQuery(QString());
|
||||
} else {
|
||||
cursor.setPosition(cursor.position() - suggestion.size(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(replacement + ' ');
|
||||
}
|
||||
}
|
||||
|
||||
void SuggestionsController::handleCursorPositionChange() {
|
||||
InvokeQueued(this, [this] {
|
||||
if (_ignoreCursorPositionChange) {
|
||||
return;
|
||||
}
|
||||
_suggestions->showWithQuery(QString());
|
||||
});
|
||||
}
|
||||
|
||||
void SuggestionsController::suggestionsUpdated(bool visible) {
|
||||
_shown = visible;
|
||||
if (_shown) {
|
||||
_container->resizeToContent();
|
||||
updateGeometry();
|
||||
if (!_forceHidden) {
|
||||
_container->showAnimated(Ui::PanelAnimation::Origin::BottomLeft);
|
||||
}
|
||||
} 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() {
|
||||
_forceHidden = !_field->isVisible();
|
||||
if (_forceHidden) {
|
||||
_container->hideFast();
|
||||
} else if (_shown) {
|
||||
_container->showFast();
|
||||
}
|
||||
}
|
||||
|
||||
bool SuggestionsController::eventFilter(QObject *object, QEvent *event) {
|
||||
if (object == _field) {
|
||||
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: {
|
||||
updateForceHidden();
|
||||
} break;
|
||||
|
||||
case QEvent::KeyPress: {
|
||||
auto key = static_cast<QKeyEvent*>(event)->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;
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
if (_shown && !_forceHidden) {
|
||||
_suggestions->showWithQuery(QString());
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_textChangeAfterKeyPress = true;
|
||||
InvokeQueued(this, [this] { _textChangeAfterKeyPress = false; });
|
||||
} break;
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void SuggestionsController::raise() {
|
||||
_container->raise();
|
||||
}
|
||||
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
||||
|
@ -20,3 +20,89 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "chat_helpers/emoji_suggestions.h"
|
||||
#include "ui/effects/panel_animation.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class InnerDropdown;
|
||||
class FlatTextarea;
|
||||
|
||||
namespace Emoji {
|
||||
|
||||
class SuggestionsWidget : public TWidget {
|
||||
public:
|
||||
SuggestionsWidget(QWidget *parent, const style::Menu &st);
|
||||
|
||||
void showWithQuery(const QString &query);
|
||||
void handleKeyEvent(int key);
|
||||
|
||||
base::Observable<bool> toggleAnimated;
|
||||
base::Observable<QString> triggered;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void enterEventHook(QEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
private:
|
||||
class Row;
|
||||
|
||||
void resizeToRows();
|
||||
int countWidth(const Row &row);
|
||||
void setSelected(int selected);
|
||||
void setPressed(int pressed);
|
||||
void clearMouseSelection();
|
||||
void clearSelection();
|
||||
void updateSelectedItem();
|
||||
int itemTop(int index);
|
||||
void updateItem(int index);
|
||||
void updateSelection(QPoint globalPosition);
|
||||
|
||||
gsl::not_null<const style::Menu*> _st;
|
||||
|
||||
QString _query;
|
||||
std::vector<Row> _rows;
|
||||
|
||||
int _rowHeight = 0;
|
||||
bool _mouseSelection = false;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
|
||||
};
|
||||
|
||||
class SuggestionsController : public QObject, private base::Subscriber {
|
||||
public:
|
||||
SuggestionsController(QWidget *parent, gsl::not_null<QTextEdit*> field);
|
||||
|
||||
void raise();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
|
||||
private:
|
||||
void handleCursorPositionChange();
|
||||
void handleTextChange();
|
||||
QString getEmojiQuery();
|
||||
void suggestionsUpdated(bool visible);
|
||||
void updateGeometry();
|
||||
void updateForceHidden();
|
||||
void replaceCurrent(const QString &replacement);
|
||||
|
||||
bool _shown = false;
|
||||
bool _forceHidden = false;
|
||||
int _queryStartPosition = 0;
|
||||
bool _ignoreCursorPositionChange = false;
|
||||
bool _textChangeAfterKeyPress = false;
|
||||
QPointer<QTextEdit> _field;
|
||||
object_ptr<InnerDropdown> _container;
|
||||
QPointer<SuggestionsWidget> _suggestions;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
||||
|
@ -73,6 +73,102 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "window/notifications_manager.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "inline_bots/inline_results_widget.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
|
||||
// Smart pointer for QObject*, has move semantics, destroys object if it doesn't have a parent.
|
||||
template <typename Object>
|
||||
class test_ptr {
|
||||
public:
|
||||
test_ptr(std::nullptr_t) {
|
||||
}
|
||||
|
||||
// No default constructor, but constructors with at least
|
||||
// one argument are simply make functions.
|
||||
template <typename Parent, typename... Args>
|
||||
explicit test_ptr(Parent &&parent, Args&&... args) : _object(new Object(std::forward<Parent>(parent), std::forward<Args>(args)...)) {
|
||||
}
|
||||
|
||||
test_ptr(const test_ptr &other) = delete;
|
||||
test_ptr &operator=(const test_ptr &other) = delete;
|
||||
test_ptr(test_ptr &&other) : _object(base::take(other._object)) {
|
||||
}
|
||||
test_ptr &operator=(test_ptr &&other) {
|
||||
auto temp = std::move(other);
|
||||
destroy();
|
||||
std::swap(_object, temp._object);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename OtherObject, typename = std::enable_if_t<std::is_base_of<Object, OtherObject>::value>>
|
||||
test_ptr(test_ptr<OtherObject> &&other) : _object(base::take(other._object)) {
|
||||
}
|
||||
|
||||
template <typename OtherObject, typename = std::enable_if_t<std::is_base_of<Object, OtherObject>::value>>
|
||||
test_ptr &operator=(test_ptr<OtherObject> &&other) {
|
||||
_object = base::take(other._object);
|
||||
return *this;
|
||||
}
|
||||
|
||||
test_ptr &operator=(std::nullptr_t) {
|
||||
_object = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// So we can pass this pointer to methods like connect().
|
||||
Object *data() const {
|
||||
return static_cast<Object*>(_object);
|
||||
}
|
||||
operator Object*() const {
|
||||
return data();
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return _object != nullptr;
|
||||
}
|
||||
|
||||
Object *operator->() const {
|
||||
return data();
|
||||
}
|
||||
Object &operator*() const {
|
||||
return *data();
|
||||
}
|
||||
|
||||
// Use that instead "= new Object(parent, ...)"
|
||||
template <typename Parent, typename... Args>
|
||||
void create(Parent &&parent, Args&&... args) {
|
||||
destroy();
|
||||
_object = new Object(std::forward<Parent>(parent), std::forward<Args>(args)...);
|
||||
}
|
||||
void destroy() {
|
||||
delete base::take(_object);
|
||||
}
|
||||
void destroyDelayed() {
|
||||
if (_object) {
|
||||
if (auto widget = base::up_cast<QWidget*>(data())) {
|
||||
widget->hide();
|
||||
}
|
||||
base::take(_object)->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
~test_ptr() {
|
||||
if (auto pointer = _object) {
|
||||
if (!pointer->parent()) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename OtherObject>
|
||||
friend class test_ptr;
|
||||
|
||||
QPointer<QObject> _object;
|
||||
|
||||
};
|
||||
|
||||
class TestClass;
|
||||
test_ptr<TestClass> tmp = { nullptr };
|
||||
|
||||
namespace {
|
||||
|
||||
@ -620,6 +716,7 @@ HistoryWidget::HistoryWidget(QWidget *parent, gsl::not_null<Window::Controller*>
|
||||
_field->setInsertFromMimeDataHook([this](const QMimeData *data) {
|
||||
return confirmSendingFiles(data, CompressConfirm::Auto, data->text());
|
||||
});
|
||||
_emojiSuggestions.create(this, _field.data());
|
||||
updateFieldSubmitSettings();
|
||||
|
||||
_field->hide();
|
||||
@ -930,6 +1027,7 @@ void HistoryWidget::orderWidgets() {
|
||||
if (_tabbedPanel) {
|
||||
_tabbedPanel->raise();
|
||||
}
|
||||
_emojiSuggestions->raise();
|
||||
if (_tabbedSelectorToggleTooltip) {
|
||||
_tabbedSelectorToggleTooltip->raise();
|
||||
}
|
||||
@ -4299,9 +4397,9 @@ void HistoryWidget::onFieldFocused() {
|
||||
void HistoryWidget::onCheckFieldAutocomplete() {
|
||||
if (!_history || _a_show.animating()) return;
|
||||
|
||||
bool start = false;
|
||||
bool isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot);
|
||||
QString query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start);
|
||||
auto start = false;
|
||||
auto isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot);
|
||||
auto query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start);
|
||||
if (!query.isEmpty()) {
|
||||
if (query.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtagsAndBots();
|
||||
if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots();
|
||||
|
@ -49,6 +49,9 @@ class SendButton;
|
||||
class FlatButton;
|
||||
class LinkButton;
|
||||
class RoundButton;
|
||||
namespace Emoji {
|
||||
class SuggestionsController;
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
@ -833,6 +836,8 @@ private:
|
||||
DragState _attachDrag = DragStateNone;
|
||||
object_ptr<DragArea> _attachDragDocument, _attachDragPhoto;
|
||||
|
||||
object_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions = { nullptr };
|
||||
|
||||
bool _nonEmptySelection = false;
|
||||
|
||||
TaskQueue _fileLoader;
|
||||
|
@ -454,7 +454,7 @@ public:
|
||||
|
||||
// So we can pass this pointer to methods like connect().
|
||||
Object *data() const {
|
||||
return static_cast<Object*>(_object);
|
||||
return static_cast<Object*>(_object.data());
|
||||
}
|
||||
operator Object*() const {
|
||||
return data();
|
||||
@ -504,14 +504,14 @@ private:
|
||||
template <typename OtherObject>
|
||||
friend class object_ptr;
|
||||
|
||||
QObject *_object = nullptr;
|
||||
QPointer<QObject> _object;
|
||||
|
||||
};
|
||||
|
||||
template <typename ResultType, typename SourceType>
|
||||
inline object_ptr<ResultType> static_object_cast(object_ptr<SourceType> source) {
|
||||
auto result = object_ptr<ResultType>(nullptr);
|
||||
result._object = static_cast<ResultType*>(base::take(source._object));
|
||||
result._object = static_cast<ResultType*>(base::take(source._object).data());
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,8 @@ void InnerDropdown::resizeToContent() {
|
||||
}
|
||||
if (newWidth != width() || newHeight != height()) {
|
||||
resize(newWidth, newHeight);
|
||||
update();
|
||||
finishAnimations();
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,30 +131,38 @@ void InnerDropdown::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
void InnerDropdown::enterEventHook(QEvent *e) {
|
||||
showAnimated(_origin);
|
||||
if (_autoHiding) {
|
||||
showAnimated(_origin);
|
||||
}
|
||||
return TWidget::enterEventHook(e);
|
||||
}
|
||||
|
||||
void InnerDropdown::leaveEventHook(QEvent *e) {
|
||||
auto ms = getms();
|
||||
if (_a_show.animating(ms) || _a_opacity.animating(ms)) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.start(300);
|
||||
if (_autoHiding) {
|
||||
auto ms = getms();
|
||||
if (_a_show.animating(ms) || _a_opacity.animating(ms)) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.start(300);
|
||||
}
|
||||
}
|
||||
return TWidget::leaveEventHook(e);
|
||||
}
|
||||
|
||||
void InnerDropdown::otherEnter() {
|
||||
showAnimated(_origin);
|
||||
if (_autoHiding) {
|
||||
showAnimated(_origin);
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::otherLeave() {
|
||||
auto ms = getms();
|
||||
if (_a_show.animating(ms) || _a_opacity.animating(ms)) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.start(0);
|
||||
if (_autoHiding) {
|
||||
auto ms = getms();
|
||||
if (_a_show.animating(ms) || _a_opacity.animating(ms)) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.start(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,6 +172,10 @@ void InnerDropdown::setOrigin(PanelAnimation::Origin origin) {
|
||||
|
||||
void InnerDropdown::showAnimated(PanelAnimation::Origin origin) {
|
||||
setOrigin(origin);
|
||||
showAnimated();
|
||||
}
|
||||
|
||||
void InnerDropdown::showAnimated() {
|
||||
_hideTimer.stop();
|
||||
showStarted();
|
||||
}
|
||||
@ -177,17 +191,43 @@ void InnerDropdown::hideAnimated(HideOption option) {
|
||||
startOpacityAnimation(true);
|
||||
}
|
||||
|
||||
void InnerDropdown::finishAnimations() {
|
||||
if (_a_show.animating()) {
|
||||
_a_show.finish();
|
||||
showAnimationCallback();
|
||||
}
|
||||
if (_showAnimation) {
|
||||
_showAnimation.reset();
|
||||
showChildren();
|
||||
}
|
||||
if (_a_opacity.animating()) {
|
||||
_a_opacity.finish();
|
||||
opacityAnimationCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::showFast() {
|
||||
_hideTimer.stop();
|
||||
finishAnimations();
|
||||
if (isHidden()) {
|
||||
showChildren();
|
||||
show();
|
||||
}
|
||||
_hiding = false;
|
||||
}
|
||||
|
||||
void InnerDropdown::hideFast() {
|
||||
if (isHidden()) return;
|
||||
|
||||
_hideTimer.stop();
|
||||
finishAnimations();
|
||||
_hiding = false;
|
||||
_a_opacity.finish();
|
||||
hideFinished();
|
||||
}
|
||||
|
||||
void InnerDropdown::hideFinished() {
|
||||
_a_show.finish();
|
||||
_showAnimation.reset();
|
||||
_cache = QPixmap();
|
||||
_ignoreShowEvents = false;
|
||||
if (!isHidden()) {
|
||||
|
@ -45,12 +45,14 @@ public:
|
||||
return rect().marginsRemoved(_st.padding).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
|
||||
}
|
||||
|
||||
void setAutoHiding(bool autoHiding) {
|
||||
_autoHiding = autoHiding;
|
||||
}
|
||||
void setMaxHeight(int newMaxHeight);
|
||||
void resizeToContent();
|
||||
|
||||
void otherEnter();
|
||||
void otherLeave();
|
||||
void hideFast();
|
||||
|
||||
void setShowStartCallback(base::lambda<void()> callback) {
|
||||
_showStartCallback = std::move(callback);
|
||||
@ -66,13 +68,17 @@ public:
|
||||
return _hiding && _a_opacity.animating();
|
||||
}
|
||||
|
||||
void setOrigin(PanelAnimation::Origin origin);
|
||||
void showAnimated(PanelAnimation::Origin origin);
|
||||
enum class HideOption {
|
||||
Default,
|
||||
IgnoreShow,
|
||||
};
|
||||
void showAnimated();
|
||||
void setOrigin(PanelAnimation::Origin origin);
|
||||
void showAnimated(PanelAnimation::Origin origin);
|
||||
void hideAnimated(HideOption option = HideOption::Default);
|
||||
void finishAnimations();
|
||||
void showFast();
|
||||
void hideFast();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
@ -115,6 +121,7 @@ private:
|
||||
std::unique_ptr<PanelAnimation> _showAnimation;
|
||||
Animation _a_show;
|
||||
|
||||
bool _autoHiding = true;
|
||||
bool _hiding = false;
|
||||
QPixmap _cache;
|
||||
Animation _a_opacity;
|
||||
|
@ -99,6 +99,23 @@ private slots:
|
||||
void actionChanged();
|
||||
|
||||
private:
|
||||
struct ActionData {
|
||||
ActionData() = default;
|
||||
ActionData(const ActionData &other) = delete;
|
||||
ActionData &operator=(const ActionData &other) = delete;
|
||||
ActionData(ActionData &&other) = default;
|
||||
ActionData &operator=(ActionData &&other) = default;
|
||||
~ActionData();
|
||||
|
||||
bool hasSubmenu = false;
|
||||
QString text;
|
||||
QString shortcut;
|
||||
const style::icon *icon = nullptr;
|
||||
const style::icon *iconOver = nullptr;
|
||||
std::unique_ptr<RippleAnimation> ripple;
|
||||
std::unique_ptr<ToggleView> toggle;
|
||||
};
|
||||
|
||||
void updateSelected(QPoint globalPosition);
|
||||
void init();
|
||||
|
||||
@ -126,23 +143,6 @@ private:
|
||||
base::lambda<void(QPoint globalPosition)> _mousePressDelegate;
|
||||
base::lambda<void(QPoint globalPosition)> _mouseReleaseDelegate;
|
||||
|
||||
struct ActionData {
|
||||
ActionData() = default;
|
||||
ActionData(const ActionData &other) = delete;
|
||||
ActionData &operator=(const ActionData &other) = delete;
|
||||
ActionData(ActionData &&other) = default;
|
||||
ActionData &operator=(ActionData &&other) = default;
|
||||
~ActionData();
|
||||
|
||||
bool hasSubmenu = false;
|
||||
QString text;
|
||||
QString shortcut;
|
||||
const style::icon *icon = nullptr;
|
||||
const style::icon *iconOver = nullptr;
|
||||
std::unique_ptr<RippleAnimation> ripple;
|
||||
std::unique_ptr<ToggleView> toggle;
|
||||
};
|
||||
|
||||
QMenu *_wappedMenu = nullptr;
|
||||
Actions _actions;
|
||||
std::vector<ActionData> _actionsData;
|
||||
|
@ -295,6 +295,13 @@ themeEditorDescriptionSkip: 10px;
|
||||
themeEditorNameFont: font(15px semibold);
|
||||
themeEditorCopyNameFont: font(fsize semibold);
|
||||
|
||||
windowEmojiSuggestionsPopup: PopupMenu(defaultPopupMenu) {
|
||||
menu: Menu(defaultMenu) {
|
||||
itemPadding: margins(48px, 8px, 17px, 7px);
|
||||
widthMax: 512px;
|
||||
}
|
||||
}
|
||||
|
||||
// Mac specific
|
||||
|
||||
macAccessoryWidth: 450.;
|
||||
|
Loading…
Reference in New Issue
Block a user