tdesktop/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp

2742 lines
75 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "chat_helpers/emoji_list_widget.h"
#include "api/api_peer_photo.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/tabbed_search.h"
#include "ui/text/format_values.h"
#include "ui/effects/animations.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/premium_graphics.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "boxes/sticker_set_box.h"
#include "lang/lang_keys.h"
#include "layout/layout_position.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_peer_values.h"
#include "data/stickers/data_stickers.h"
#include "data/stickers/data_custom_emoji.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_list_widget.h"
#include "chat_helpers/stickers_list_footer.h"
#include "emoji_suggestions_data.h"
#include "emoji_suggestions_helper.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "settings/settings_premium.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace {
constexpr auto kCollapsedRows = 3;
constexpr auto kAppearDuration = 0.3;
constexpr auto kCustomSearchLimit = 256;
constexpr auto kColorPickerDelay = crl::time(500);
using Core::RecentEmojiId;
using Core::RecentEmojiDocument;
} // namespace
class EmojiColorPicker final : public Ui::RpWidget {
public:
EmojiColorPicker(QWidget *parent, const style::EmojiPan &st);
void showEmoji(EmojiPtr emoji, bool allLabel = false);
void clearSelection();
void handleMouseMove(QPoint globalPos);
void handleMouseRelease(QPoint globalPos);
void setSingleSize(QSize size);
void showAnimated();
void hideAnimated();
void hideFast();
[[nodiscard]] rpl::producer<EmojiChosen> chosen() const;
[[nodiscard]] rpl::producer<> hidden() const;
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
private:
void createAllLabel();
void animationCallback();
void updateSize();
[[nodiscard]] int topColorAllSkip() const;
void drawVariant(QPainter &p, int variant);
void updateSelected();
void setSelected(int newSelected);
const style::EmojiPan &_st;
bool _ignoreShow = false;
QVector<EmojiPtr> _variants;
int _selected = -1;
int _pressedSel = -1;
QPoint _lastMousePos;
QSize _singleSize;
QPoint _areaPosition;
QPoint _innerPosition;
Ui::RoundRect _backgroundRect;
Ui::RoundRect _overBg;
bool _hiding = false;
QPixmap _cache;
Ui::Animations::Simple _a_opacity;
std::unique_ptr<Ui::FlatLabel> _allLabel;
rpl::event_stream<EmojiChosen> _chosen;
rpl::event_stream<> _hidden;
};
struct EmojiListWidget::CustomEmojiInstance {
std::unique_ptr<Ui::Text::CustomEmoji> emoji;
bool recentOnly = false;
};
struct EmojiListWidget::RecentOne {
Ui::Text::CustomEmoji *custom = nullptr;
RecentEmojiId id;
};
EmojiColorPicker::EmojiColorPicker(
QWidget *parent,
const style::EmojiPan &st)
: RpWidget(parent)
, _st(st)
, _backgroundRect(st::emojiPanRadius, _st.bg)
, _overBg(st::emojiPanRadius, _st.overBg) {
setMouseTracking(true);
}
void EmojiColorPicker::showEmoji(EmojiPtr emoji, bool allLabel) {
if (!emoji || !emoji->hasVariants()) {
return;
}
if (!allLabel) {
_allLabel = nullptr;
} else if (!_allLabel) {
createAllLabel();
}
_ignoreShow = false;
_variants.resize(emoji->variantsCount() + 1);
for (auto i = 0, size = int(_variants.size()); i != size; ++i) {
_variants[i] = emoji->variant(i);
}
updateSize();
if (!_cache.isNull()) {
_cache = QPixmap();
}
showAnimated();
}
void EmojiColorPicker::createAllLabel() {
_allLabel = std::make_unique<Ui::FlatLabel>(
this,
tr::lng_emoji_color_all(),
_st.colorAllLabel);
_allLabel->show();
_allLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
}
void EmojiColorPicker::updateSize() {
auto width = st::emojiPanMargins.left()
+ _singleSize.width() * _variants.size()
+ (_variants.size() - 2) * st::emojiColorsPadding
+ st::emojiColorsSep
+ st::emojiPanMargins.right();
auto height = st::emojiPanMargins.top()
+ 2 * st::emojiColorsPadding
+ _singleSize.height()
+ st::emojiPanMargins.bottom();
if (_allLabel) {
_allLabel->resizeToWidth(width
- st::emojiPanMargins.left()
- st::emojiPanMargins.right()
- st::emojiPanColorAllPadding.left()
- st::emojiPanColorAllPadding.right());
_allLabel->move(
st::emojiPanMargins.left() + st::emojiPanColorAllPadding.left(),
st::emojiPanMargins.top() + st::emojiPanColorAllPadding.top());
height += topColorAllSkip();
}
resize(width, height);
update();
updateSelected();
}
void EmojiColorPicker::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
if (opacity < 1.) {
if (opacity > 0.) {
p.setOpacity(opacity);
} else {
return;
}
}
if (e->rect() != rect()) {
p.setClipRect(e->rect());
}
auto inner = rect().marginsRemoved(st::emojiPanMargins);
if (!_cache.isNull()) {
p.drawPixmap(0, 0, _cache);
return;
}
Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow);
_backgroundRect.paint(p, inner);
const auto skip = topColorAllSkip();
auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width();
if (rtl()) x = width() - x - st::emojiColorsSep;
p.fillRect(x, st::emojiPanMargins.top() + skip + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2 - skip, st::emojiColorsSepColor);
if (_variants.isEmpty()) {
return;
}
p.translate(0, skip);
for (auto i = 0, count = int(_variants.size()); i != count; ++i) {
drawVariant(p, i);
}
}
void EmojiColorPicker::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
_lastMousePos = e->globalPos();
updateSelected();
_pressedSel = _selected;
}
void EmojiColorPicker::mouseReleaseEvent(QMouseEvent *e) {
handleMouseRelease(e->globalPos());
}
void EmojiColorPicker::handleMouseRelease(QPoint globalPos) {
_lastMousePos = globalPos;
int32 pressed = _pressedSel;
_pressedSel = -1;
updateSelected();
if (_selected >= 0 && (pressed < 0 || _selected == pressed)) {
_chosen.fire_copy({ .emoji = _variants[_selected] });
}
_ignoreShow = true;
hideAnimated();
}
void EmojiColorPicker::setSingleSize(QSize size) {
const auto area = st::emojiPanArea;
_singleSize = size;
_areaPosition = QPoint(
(_singleSize.width() - area.width()) / 2,
(_singleSize.height() - area.height()) / 2);
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
_innerPosition = QPoint(
(area.width() - esize) / 2,
(area.height() - esize) / 2);
updateSize();
}
void EmojiColorPicker::handleMouseMove(QPoint globalPos) {
_lastMousePos = globalPos;
updateSelected();
}
void EmojiColorPicker::mouseMoveEvent(QMouseEvent *e) {
handleMouseMove(e->globalPos());
}
void EmojiColorPicker::animationCallback() {
update();
if (!_a_opacity.animating()) {
_cache = QPixmap();
if (_allLabel) {
_allLabel->show();
}
if (_hiding) {
hide();
_hidden.fire({});
} else {
_lastMousePos = QCursor::pos();
updateSelected();
}
}
}
void EmojiColorPicker::hideFast() {
clearSelection();
_a_opacity.stop();
_cache = QPixmap();
hide();
_hidden.fire({});
}
rpl::producer<EmojiChosen> EmojiColorPicker::chosen() const {
return _chosen.events();
}
rpl::producer<> EmojiColorPicker::hidden() const {
return _hidden.events();
}
void EmojiColorPicker::hideAnimated() {
if (_cache.isNull()) {
if (_allLabel) {
_allLabel->show();
}
_cache = Ui::GrabWidget(this);
clearSelection();
}
_hiding = true;
if (_allLabel) {
_allLabel->hide();
}
_a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration);
}
void EmojiColorPicker::showAnimated() {
if (_ignoreShow) return;
if (!isHidden() && !_hiding) {
return;
}
_hiding = false;
if (_cache.isNull()) {
if (_allLabel) {
_allLabel->show();
}
_cache = Ui::GrabWidget(this);
clearSelection();
}
show();
if (_allLabel) {
_allLabel->hide();
}
_a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration);
}
void EmojiColorPicker::clearSelection() {
_pressedSel = -1;
setSelected(-1);
_lastMousePos = mapToGlobal(QPoint(-10, -10));
}
int EmojiColorPicker::topColorAllSkip() const {
return _allLabel
? (st::emojiPanColorAllPadding.top()
+ _allLabel->height()
+ st::emojiPanColorAllPadding.bottom())
: 0;
}
void EmojiColorPicker::updateSelected() {
auto newSelected = -1;
auto p = mapFromGlobal(_lastMousePos);
auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - topColorAllSkip() - st::emojiColorsPadding;
if (y >= 0 && y < _singleSize.height()) {
auto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding;
if (x >= 0 && x < _singleSize.width()) {
newSelected = 0;
} else {
x -= _singleSize.width() + 2 * st::emojiColorsPadding + st::emojiColorsSep;
if (x >= 0 && x < _singleSize.width() * (_variants.size() - 1)) {
newSelected = (x / _singleSize.width()) + 1;
}
}
}
setSelected(newSelected);
}
void EmojiColorPicker::setSelected(int newSelected) {
if (_selected == newSelected) {
return;
}
const auto skip = topColorAllSkip();
const auto updateSelectedRect = [&] {
if (_selected < 0) return;
auto addedSkip = (_selected > 0)
? (2 * st::emojiColorsPadding + st::emojiColorsSep)
: 0;
auto left = st::emojiPanMargins.left()
+ st::emojiColorsPadding
+ _selected * _singleSize.width()
+ addedSkip;
rtlupdate(
left,
st::emojiPanMargins.top() + st::emojiColorsPadding + skip,
_singleSize.width(),
_singleSize.height());
};
updateSelectedRect();
_selected = newSelected;
updateSelectedRect();
setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default);
}
void EmojiColorPicker::drawVariant(QPainter &p, int variant) {
const auto w = QPoint(
st::emojiPanMargins.left(),
st::emojiPanMargins.top()
) + QPoint(
(st::emojiColorsPadding
+ variant * _singleSize.width()
+ (variant
? (2 * st::emojiColorsPadding + st::emojiColorsSep)
: 0)),
st::emojiColorsPadding
) + _areaPosition;
if (variant == _selected) {
QPoint tl(w);
if (rtl()) tl.setX(width() - tl.x() - st::emojiPanArea.width());
_overBg.paint(p, QRect(tl, st::emojiPanArea));
}
Ui::Emoji::Draw(
p,
_variants[variant],
Ui::Emoji::GetSizeLarge(),
w.x() + _innerPosition.x(),
w.y() + _innerPosition.y());
}
EmojiListWidget::EmojiListWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
PauseReason level,
Mode mode)
: EmojiListWidget(parent, {
.show = controller->uiShow(),
.mode = mode,
.paused = Window::PausedIn(controller, level),
}) {
}
EmojiListWidget::EmojiListWidget(
QWidget *parent,
EmojiListDescriptor &&descriptor)
: Inner(
parent,
descriptor.st ? *descriptor.st : st::defaultEmojiPan,
descriptor.show,
std::move(descriptor.paused))
, _show(std::move(descriptor.show))
, _features(descriptor.features)
, _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle)
, _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode)
, _api(&session().mtp())
, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)
, _premiumIcon(_mode == Mode::EmojiStatus
? std::make_unique<GradientPremiumStar>()
: nullptr)
, _localSetsManager(
std::make_unique<LocalStickersManager>(&session()))
, _customRecentFactory(std::move(descriptor.customRecentFactory))
, _customTextColor(std::move(descriptor.customTextColor))
, _overBg(st::emojiPanRadius, st().overBg)
, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg)
, _picker(this, st())
, _showPickerTimer([=] { showPicker(); })
, _previewTimer([=] { showPreview(); }) {
setMouseTracking(true);
if (st().bg->c.alpha() > 0) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
if (_mode != Mode::RecentReactions
&& _mode != Mode::BackgroundEmoji
&& _mode != Mode::ChannelStatus
&& !_onlyUnicodeEmoji) {
setupSearch();
}
if (_mode == Mode::ChannelStatus) {
session().api().peerPhoto().emojiListValue(
Api::PeerPhoto::EmojiListType::NoChannelStatus
) | rpl::start_with_next([=](const std::vector<DocumentId> &list) {
_restrictedCustomList = { begin(list), end(list) };
if (!_custom.empty()) {
refreshCustom();
}
}, lifetime());
}
_customSingleSize = Data::FrameSizeFromTag(
Data::CustomEmojiManager::SizeTag::Large
) / style::DevicePixelRatio();
_picker->hide();
for (auto i = 1; i != _staticCount; ++i) {
const auto section = static_cast<Section>(i);
_counts[i] = Ui::Emoji::GetSectionCount(section);
}
_picker->chosen(
) | rpl::start_with_next([=](EmojiChosen data) {
colorChosen(data);
}, lifetime());
_picker->hidden(
) | rpl::start_with_next([=] {
pickerHidden();
}, lifetime());
session().changes().peerUpdates(
Data::PeerUpdate::Flag::EmojiSet
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer.get() == _megagroupSet);
}) | rpl::start_with_next([=] {
refreshCustom();
resizeToWidth(width());
}, lifetime());
session().data().stickers().updated(
Data::StickersType::Emoji
) | rpl::start_with_next([=] {
refreshCustom();
resizeToWidth(width());
}, lifetime());
rpl::combine(
Data::AmPremiumValue(&session()),
session().premiumPossibleValue()
) | rpl::skip(1) | rpl::start_with_next([=] {
refreshCustom();
resizeToWidth(width());
}, lifetime());
rpl::single(
rpl::empty
) | rpl::then(
style::PaletteChanged()
) | rpl::start_with_next([=] {
initButton(_add, tr::lng_stickers_featured_add(tr::now), false);
initButton(_unlock, tr::lng_emoji_featured_unlock(tr::now), true);
initButton(_restore, tr::lng_emoji_premium_restore(tr::now), true);
}, lifetime());
if (!descriptor.customRecentList.empty()) {
fillRecentFrom(descriptor.customRecentList);
}
}
EmojiListWidget::~EmojiListWidget() {
base::take(_customEmoji);
}
void EmojiListWidget::setupSearch() {
const auto session = &_show->session();
_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {
_nextSearchQuery = std::move(query);
InvokeQueued(this, [=] {
applyNextSearchQuery();
});
}, session, (_mode == Mode::EmojiStatus), _mode == Mode::UserpicBuilder);
}
void EmojiListWidget::applyNextSearchQuery() {
if (_searchQuery == _nextSearchQuery) {
return;
}
_searchQuery = _nextSearchQuery;
std::swap(_searchEmoji, _searchEmojiPrevious);
_searchEmoji.clear();
const auto finish = [&](bool searching = true) {
if (!_searchMode && !searching) {
return;
}
const auto modeChanged = (_searchMode != searching);
clearSelection();
if (modeChanged) {
_searchMode = searching;
}
if (!searching) {
_searchResults.clear();
_searchCustomIds.clear();
}
resizeToWidth(width());
update();
if (modeChanged) {
visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom());
}
updateSelected();
};
if (_searchQuery.empty()) {
finish(false);
return;
}
const auto guard = gsl::finally([&] { finish(); });
auto plain = collectPlainSearchResults();
if (_searchEmoji == _searchEmojiPrevious) {
return;
}
_searchResults.clear();
_searchCustomIds.clear();
if (_mode != Mode::Full || session().premium()) {
appendPremiumSearchResults();
}
if (_mode == Mode::Full) {
for (const auto emoji : plain) {
_searchResults.push_back({
.id = { emoji },
});
}
}
}
void EmojiListWidget::showPreview() {
if (const auto over = std::get_if<OverEmoji>(&_pressed)) {
if (const auto custom = lookupCustomEmoji(over)) {
_show->showMediaPreview(custom->stickerSetOrigin(), custom);
_previewShown = true;
}
}
}
std::vector<EmojiPtr> EmojiListWidget::collectPlainSearchResults() {
return SearchEmoji(_searchQuery, _searchEmoji);
}
void EmojiListWidget::appendPremiumSearchResults() {
const auto test = session().isTestMode();
auto &owner = session().data();
const auto checkCustom = [&](EmojiPtr emoji, DocumentId id) {
return emoji
&& _searchEmoji.contains(emoji)
&& (_searchResults.size() < kCustomSearchLimit)
&& _searchCustomIds.emplace(id).second;
};
for (const auto &recent : _recent) {
if (!recent.custom) {
continue;
}
const auto &idData = recent.id.data;
const auto id = std::get_if<Core::RecentEmojiDocument>(&idData);
if (!id || id->test != test) {
continue;
}
const auto sticker = owner.document(id->id)->sticker();
const auto emoji = sticker
? Ui::Emoji::Find(sticker->alt)
: nullptr;
if (checkCustom(emoji, id->id)) {
_searchResults.push_back(recent);
}
}
for (const auto &set : _custom) {
for (const auto &one : set.list) {
const auto id = one.document->id;
if (checkCustom(one.emoji, id)) {
_searchResults.push_back({
.custom = one.custom,
.id = { RecentEmojiDocument{ .id = id, .test = test } },
});
}
}
}
}
void EmojiListWidget::provideRecent(
const std::vector<DocumentId> &customRecentList) {
clearSelection();
fillRecentFrom(customRecentList);
resizeToWidth(width());
}
void EmojiListWidget::repaintCustom(uint64 setId) {
if (!_repaintsScheduled.emplace(setId).second) {
return;
}
const auto repaintSearch = (setId == SearchEmojiSectionSetId());
if (_searchMode) {
if (repaintSearch) {
update();
}
return;
}
const auto repaintRecent = (setId == RecentEmojiSectionSetId());
enumerateSections([&](const SectionInfo &info) {
const auto repaint1 = repaintRecent
&& (info.section == int(Section::Recent));
const auto repaint2 = !repaint1
&& (info.section >= _staticCount)
&& (setId == _custom[info.section - _staticCount].id);
if (repaint1 || repaint2) {
update(
0,
info.rowsTop,
width(),
info.rowsBottom - info.rowsTop);
}
return true;
});
}
rpl::producer<EmojiChosen> EmojiListWidget::chosen() const {
return _chosen.events();
}
rpl::producer<FileChosen> EmojiListWidget::customChosen() const {
return _customChosen.events();
}
rpl::producer<> EmojiListWidget::jumpedToPremium() const {
return _jumpedToPremium.events();
}
rpl::producer<> EmojiListWidget::escapes() const {
return _search ? _search->escapes() : rpl::never<>();
}
void EmojiListWidget::prepareExpanding() {
if (_search) {
_searchExpandCache = _search->grab();
}
}
void EmojiListWidget::paintExpanding(
Painter &p,
QRect clip,
int finalBottom,
float64 geometryProgress,
float64 fullProgress,
RectPart origin) {
const auto searchShift = _search
? anim::interpolate(
st().padding.top() - _search->height(),
0,
geometryProgress)
: 0;
const auto shift = clip.topLeft() + QPoint(0, searchShift);
const auto adjusted = clip.translated(-shift);
const auto finalHeight = (finalBottom - clip.y());
if (!_searchExpandCache.isNull()) {
p.setClipRect(clip);
p.drawImage(
clip.x() + st().searchMargin.left(),
clip.y() + st().searchMargin.top() + searchShift,
_searchExpandCache);
}
p.translate(shift);
p.setClipRect(adjusted);
paint(p, ExpandingContext{
.progress = fullProgress,
.finalHeight = finalHeight,
.expanding = true,
}, adjusted);
p.translate(-shift);
}
void EmojiListWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
Inner::visibleTopBottomUpdated(visibleTop, visibleBottom);
if (_footer) {
_footer->validateSelectedIcon(
currentSet(visibleTop),
ValidateIconAnimations::Full);
}
unloadNotSeenCustom(visibleTop, visibleBottom);
}
void EmojiListWidget::unloadNotSeenCustom(
int visibleTop,
int visibleBottom) {
enumerateSections([&](const SectionInfo &info) {
if (info.rowsBottom <= visibleTop || info.rowsTop >= visibleBottom) {
unloadCustomIn(info);
}
return true;
});
}
void EmojiListWidget::unloadAllCustom() {
enumerateSections([&](const SectionInfo &info) {
unloadCustomIn(info);
return true;
});
}
void EmojiListWidget::unloadCustomIn(const SectionInfo &info) {
if (!info.section && _recentPainted) {
_recentPainted = false;
for (const auto &single : _recent) {
if (const auto custom = single.custom) {
custom->unload();
}
}
return;
} else if (info.section < _staticCount) {
return;
}
auto &custom = _custom[info.section - _staticCount];
if (!custom.painted) {
return;
}
custom.painted = false;
for (const auto &single : custom.list) {
single.custom->unload();
}
}
object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {
Expects(_footer == nullptr);
if (_mode == EmojiListMode::RecentReactions) {
return { nullptr };
}
using FooterDescriptor = StickersListFooter::Descriptor;
const auto flag = powerSavingFlag();
const auto footerPaused = [method = pausedMethod(), flag]() {
return On(flag) || method();
};
auto result = object_ptr<StickersListFooter>(FooterDescriptor{
.session = &session(),
.customTextColor = _customTextColor,
.paused = footerPaused,
.parent = this,
.st = &st(),
.features = { .stickersSettings = false },
.forceFirstFrame = (_mode == Mode::BackgroundEmoji),
});
_footer = result;
_footer->setChosen(
) | rpl::start_with_next([=](uint64 setId) {
showSet(setId);
}, _footer->lifetime());
return result;
}
void EmojiListWidget::afterShown() {
const auto steal = (_mode == Mode::EmojiStatus)
|| (_mode == Mode::FullReactions)
|| (_mode == Mode::UserpicBuilder);
if (_search && steal) {
_search->stealFocus();
}
}
void EmojiListWidget::beforeHiding() {
if (_search) {
_search->returnFocus();
}
}
template <typename Callback>
bool EmojiListWidget::enumerateSections(Callback callback) const {
Expects(_columnCount > 0);
auto i = 0;
auto info = SectionInfo();
const auto next = [&] {
info.rowsCount = info.collapsed
? kCollapsedRows
: (info.count + _columnCount - 1) / _columnCount;
info.rowsTop = info.top
+ (i == 0 ? _rowsTop : st().header);
info.rowsBottom = info.rowsTop
+ (info.rowsCount * _singleSize.height());
if (!callback(info)) {
return false;
}
info.top = info.rowsBottom;
return true;
};
if (_searchMode) {
info.section = i;
info.count = _searchResults.size();
return next();
}
for (; i != _staticCount; ++i) {
info.section = i;
info.count = i ? _counts[i] : _recent.size();
if (!next()) {
return false;
}
}
for (auto &section : _custom) {
info.section = i++;
info.premiumRequired = section.premiumRequired;
info.count = int(section.list.size());
info.collapsed = !section.expanded
&& (!section.canRemove || section.premiumRequired)
&& (info.count > _columnCount * kCollapsedRows);
if (!next()) {
return false;
}
}
return true;
}
EmojiListWidget::SectionInfo EmojiListWidget::sectionInfo(int section) const {
Expects(section >= 0 && section < sectionsCount());
auto result = SectionInfo();
enumerateSections([&](const SectionInfo &info) {
if (info.section == section) {
result = info;
return false;
}
return true;
});
return result;
}
EmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset(
int yOffset) const {
auto result = SectionInfo();
const auto count = sectionsCount();
enumerateSections([&result, count, yOffset](const SectionInfo &info) {
if (yOffset < info.rowsBottom || info.section == count - 1) {
result = info;
return false;
}
return true;
});
return result;
}
int EmojiListWidget::sectionsCount() const {
return _searchMode ? 1 : (_staticCount + int(_custom.size()));
}
void EmojiListWidget::setSingleSize(QSize size) {
const auto area = st::emojiPanArea;
_singleSize = size;
_areaPosition = QPoint(
(_singleSize.width() - area.width()) / 2,
(_singleSize.height() - area.height()) / 2);
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
_innerPosition = QPoint(
(area.width() - esize) / 2,
(area.height() - esize) / 2);
const auto customSkip = (esize - _customSingleSize) / 2;
_customPosition = QPoint(customSkip, customSkip);
_picker->setSingleSize(_singleSize);
}
void EmojiListWidget::setColorAllForceRippled(bool force) {
_colorAllRippleForced = force;
if (_colorAllRippleForced) {
_colorAllRippleForcedLifetime = style::PaletteChanged(
) | rpl::filter([=] {
return _colorAllRipple != nullptr;
}) | rpl::start_with_next([=] {
_colorAllRipple->forceRepaint();
});
if (!_colorAllRipple) {
_colorAllRipple = createButtonRipple(int(Section::People));
}
if (_colorAllRipple->empty()) {
_colorAllRipple->addFading();
} else {
_colorAllRipple->lastUnstop();
}
} else {
if (_colorAllRipple) {
_colorAllRipple->lastStop();
}
_colorAllRippleForcedLifetime.destroy();
}
}
int EmojiListWidget::countDesiredHeight(int newWidth) {
const auto fullWidth = st().margin.left()
+ newWidth
+ st().margin.right();
const auto padding = st().padding;
const auto innerWidth = fullWidth - padding.left() - padding.right();
_columnCount = std::max(innerWidth / st().desiredSize, 1);
const auto singleWidth = innerWidth / _columnCount;
_rowsTop = _search ? _search->height() : padding.top();
_rowsLeft = padding.left()
+ (innerWidth - _columnCount * singleWidth) / 2
- st().margin.left();
setSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub });
const auto countResult = [this](int minimalLastHeight) {
const auto info = sectionInfo(sectionsCount() - 1);
return info.top
+ qMax(info.rowsBottom - info.top, minimalLastHeight);
};
const auto minimalHeight = this->minimalHeight();
const auto minimalLastHeight = std::max(
minimalHeight - padding.bottom(),
0);
return qMax(
minimalHeight,
countResult(minimalLastHeight) + padding.bottom());
}
int EmojiListWidget::defaultMinimalHeight() const {
return Inner::defaultMinimalHeight();
}
void EmojiListWidget::ensureLoaded(int section) {
Expects(section >= 0 && section < sectionsCount());
if (section == int(Section::Recent)) {
if (_recent.empty()) {
fillRecent();
}
return;
} else if (section >= _staticCount || !_emoji[section].empty()) {
return;
}
_emoji[section] = Ui::Emoji::GetSection(static_cast<Section>(section));
_counts[section] = _emoji[section].size();
const auto &settings = Core::App().settings();
for (auto &emoji : _emoji[section]) {
emoji = settings.lookupEmojiVariant(emoji);
}
}
void EmojiListWidget::fillRecent() {
if (_mode != Mode::Full) {
return;
}
_recent.clear();
_recentCustomIds.clear();
const auto &list = Core::App().settings().recentEmoji();
_recent.reserve(std::min(int(list.size()), Core::kRecentEmojiLimit) + 1);
const auto test = session().isTestMode();
for (const auto &one : list) {
const auto document = std::get_if<RecentEmojiDocument>(&one.id.data);
if (document && ((document->test != test) || _onlyUnicodeEmoji)) {
continue;
}
_recent.push_back({
.custom = resolveCustomRecent(one.id),
.id = one.id,
});
if (document) {
_recentCustomIds.emplace(document->id);
}
if (_recent.size() >= Core::kRecentEmojiLimit) {
break;
}
}
}
void EmojiListWidget::fillRecentFrom(const std::vector<DocumentId> &list) {
const auto test = session().isTestMode();
_recent.clear();
_recent.reserve(list.size());
for (const auto &id : list) {
if (!id && _mode == Mode::EmojiStatus) {
const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f");
_recent.push_back({ .id = { Ui::Emoji::Find(star) } });
} else if (!id
&& (_mode == Mode::BackgroundEmoji
|| _mode == Mode::ChannelStatus)) {
const auto fakeId = DocumentId(5246772116543512028ULL);
const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f");
_recent.push_back({
.custom = resolveCustomRecent(fakeId),
.id = { Ui::Emoji::Find(no) },
});
_recentCustomIds.emplace(fakeId);
} else {
_recent.push_back({
.custom = resolveCustomRecent(id),
.id = { RecentEmojiDocument{ .id = id, .test = test } },
});
_recentCustomIds.emplace(id);
}
}
}
base::unique_qptr<Ui::PopupMenu> EmojiListWidget::fillContextMenu(
SendMenu::Type type) {
if (v::is_null(_selected)) {
return nullptr;
}
const auto over = std::get_if<OverEmoji>(&_selected);
if (!over) {
return nullptr;
}
const auto section = over->section;
const auto index = over->index;
auto menu = base::make_unique_q<Ui::PopupMenu>(
this,
(_mode == Mode::Full
? st::popupMenuWithIcons
: st::defaultPopupMenu));
if (_mode == Mode::Full) {
fillRecentMenu(menu, section, index);
} else if (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) {
fillEmojiStatusMenu(menu, section, index);
}
if (menu->empty()) {
return nullptr;
}
return menu;
}
void EmojiListWidget::fillRecentMenu(
not_null<Ui::PopupMenu*> menu,
int section,
int index) {
if (section != int(Section::Recent)) {
return;
}
const auto addAction = Ui::Menu::CreateAddActionCallback(menu);
const auto over = OverEmoji{ section, index };
const auto emoji = lookupOverEmoji(&over);
const auto custom = lookupCustomEmoji(&over);
if (custom && custom->sticker()) {
const auto sticker = custom->sticker();
const auto emoji = sticker->alt;
const auto setId = sticker->set.id;
if (!emoji.isEmpty()) {
auto data = TextForMimeData{ emoji, { emoji } };
data.rich.entities.push_back({
EntityType::CustomEmoji,
0,
int(emoji.size()),
Data::SerializeCustomEmojiId(custom)
});
addAction(tr::lng_emoji_copy(tr::now), [=] {
TextUtilities::SetClipboardText(data);
}, &st::menuIconCopy);
}
if (setId && _features.openStickerSets) {
addAction(
tr::lng_emoji_view_pack(tr::now),
crl::guard(this, [=] { displaySet(setId); }),
&st::menuIconShowAll);
}
} else if (emoji) {
addAction(tr::lng_emoji_copy(tr::now), [=] {
const auto text = emoji->text();
TextUtilities::SetClipboardText({ text, { text } });
}, &st::menuIconCopy);
}
auto id = RecentEmojiId{ emoji };
if (custom) {
id.data = RecentEmojiDocument{
.id = custom->id,
.test = custom->session().isTestMode(),
};
}
addAction(tr::lng_emoji_remove_recent(tr::now), crl::guard(this, [=] {
Core::App().settings().hideRecentEmoji(id);
refreshRecent();
}), &st::menuIconCancel);
menu->addSeparator(&st().expandedSeparator);
const auto resetRecent = [=] {
const auto sure = [=](Fn<void()> &&close) {
Core::App().settings().resetRecentEmoji();
refreshRecent();
close();
};
checkHideWithBox(Ui::MakeConfirmBox({
.text = tr::lng_emoji_reset_recent_sure(),
.confirmed = crl::guard(this, sure),
.confirmText = tr::lng_emoji_reset_recent_button(tr::now),
.labelStyle = &st().boxLabel,
}));
};
addAction({
.text = tr::lng_emoji_reset_recent(tr::now),
.handler = crl::guard(this, resetRecent),
.icon = &st::menuIconRestoreAttention,
.isAttention = true,
});
}
void EmojiListWidget::fillEmojiStatusMenu(
not_null<Ui::PopupMenu*> menu,
int section,
int index) {
const auto chosen = lookupCustomEmoji(index, section);
if (!chosen) {
return;
}
const auto selectWith = [=](TimeId scheduled) {
selectCustom(
lookupChosen(chosen, nullptr, { .scheduled = scheduled }));
};
for (const auto &value : { 3600, 3600 * 8, 3600 * 24, 3600 * 24 * 7 }) {
const auto text = tr::lng_emoji_status_menu_duration_any(
tr::now,
lt_duration,
Ui::FormatMuteFor(value));
menu->addAction(text, crl::guard(this, [=] {
selectWith(base::unixtime::now() + value);
}));
}
menu->addAction(
tr::lng_manage_messages_ttl_after_custom(tr::now),
crl::guard(this, [=] { selectWith(
TabbedSelector::kPickCustomTimeId); }));
}
void EmojiListWidget::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e ? e->rect() : rect();
_repaintsScheduled.clear();
if (_grabbingChosen) {
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(clip, Qt::transparent);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
} else if (st().bg->c.alpha() > 0) {
p.fillRect(clip, st().bg);
}
if (!_searchExpandCache.isNull()) {
_searchExpandCache = QImage();
}
paint(p, {}, clip);
}
void EmojiListWidget::validateEmojiPaintContext(
const ExpandingContext &context) {
auto value = Ui::Text::CustomEmojiPaintContext{
.textColor = (_customTextColor
? _customTextColor()
: (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus)
? anim::color(
st::stickerPanPremium1,
st::stickerPanPremium2,
0.5)
: st().textFg->c),
.size = QSize(_customSingleSize, _customSingleSize),
.now = crl::now(),
.scale = context.progress,
.paused = On(powerSavingFlag()) || paused(),
.scaled = context.expanding,
.internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) },
};
if (!_emojiPaintContext) {
_emojiPaintContext = std::make_unique<
Ui::Text::CustomEmojiPaintContext
>(std::move(value));
} else {
*_emojiPaintContext = std::move(value);
}
}
void EmojiListWidget::paint(
Painter &p,
ExpandingContext context,
QRect clip) {
validateEmojiPaintContext(context);
auto fromColumn = floorclamp(
clip.x() - _rowsLeft,
_singleSize.width(),
0,
_columnCount);
auto toColumn = ceilclamp(
clip.x() + clip.width() - _rowsLeft,
_singleSize.width(),
0,
_columnCount);
if (rtl()) {
qSwap(fromColumn, toColumn);
fromColumn = _columnCount - fromColumn;
toColumn = _columnCount - toColumn;
}
const auto expandProgress = context.progress;
auto selectedButton = std::get_if<OverButton>(!v::is_null(_pressed)
? &_pressed
: &_selected);
if (_searchResults.empty() && _searchMode) {
paintEmptySearchResults(p);
}
enumerateSections([&](const SectionInfo &info) {
if (clip.top() >= info.rowsBottom) {
return true;
} else if (clip.top() + clip.height() <= info.top) {
return false;
}
const auto buttonSelected = selectedButton
? (selectedButton->section == info.section)
: false;
const auto titleLeft = (info.premiumRequired
? st().headerLockedLeft
: st().headerLeft) - st().margin.left();
const auto widthForTitle = emojiRight()
- titleLeft
- paintButtonGetWidth(p, info, buttonSelected, clip);
if (info.section > 0 && clip.top() < info.rowsTop) {
p.setFont(st::emojiPanHeaderFont);
p.setPen(st().headerFg);
auto titleText = (info.section < _staticCount)
? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now)
: _custom[info.section - _staticCount].title;
auto titleWidth = st::emojiPanHeaderFont->width(titleText);
if (titleWidth > widthForTitle) {
titleText = st::emojiPanHeaderFont->elided(titleText, widthForTitle);
titleWidth = st::emojiPanHeaderFont->width(titleText);
}
const auto top = info.top + st().headerTop;
if (info.premiumRequired) {
st::emojiPremiumRequired.paint(
p,
st().headerLockLeft - st().margin.left(),
top,
width());
}
const auto textBaseline = top + st::emojiPanHeaderFont->ascent;
p.setFont(st::emojiPanHeaderFont);
p.setPen(st().headerFg);
p.drawText(titleLeft, textBaseline, titleText);
}
if (clip.top() + clip.height() > info.rowsTop) {
ensureLoaded(info.section);
auto fromRow = floorclamp(
clip.y() - info.rowsTop,
_singleSize.height(),
0,
info.rowsCount);
auto toRow = ceilclamp(
clip.y() + clip.height() - info.rowsTop,
_singleSize.height(),
0,
info.rowsCount);
for (auto i = fromRow; i < toRow; ++i) {
for (auto j = fromColumn; j < toColumn; ++j) {
const auto index = i * _columnCount + j;
if (index >= info.count) {
break;
}
const auto state = OverEmoji{
.section = info.section,
.index = index,
};
const auto selected = (state == _selected)
|| (!_picker->isHidden()
&& state == _pickerSelected);
const auto position = QPoint(
_rowsLeft + j * _singleSize.width(),
info.rowsTop + i * _singleSize.height()
);
const auto w = position + _areaPosition;
if (context.expanding) {
const auto y = (position.y() - _rowsTop);
const auto x = (position.x() - _rowsLeft);
const auto sum = y
+ std::max(std::min(y, width()) - x, 0);
const auto maxSum = context.finalHeight
+ std::min(context.finalHeight, width());
const auto started = (sum / float64(maxSum))
- kAppearDuration;
context.progress = (expandProgress <= started)
? 0.
: (expandProgress >= started + kAppearDuration)
? 1.
: ((expandProgress - started) / kAppearDuration);
}
if (info.collapsed
&& index + 1 == _columnCount * kCollapsedRows) {
drawCollapsedBadge(p, w - _areaPosition, info.count);
continue;
}
if (!_grabbingChosen
&& selected
&& st().overBg->c.alpha() > 0) {
auto tl = w;
if (rtl()) {
tl.setX(width() - tl.x() - st::emojiPanArea.width());
}
_overBg.paint(p, QRect(tl, st::emojiPanArea));
}
if (_searchMode) {
drawRecent(p, context, w, _searchResults[index]);
} else if (info.section == int(Section::Recent)) {
drawRecent(p, context, w, _recent[index]);
} else if (info.section < _staticCount) {
drawEmoji(p, context, w, _emoji[info.section][index]);
} else {
const auto set = info.section - _staticCount;
drawCustom(p, context, w, set, index);
}
}
}
}
return true;
});
}
void EmojiListWidget::drawCollapsedBadge(
QPainter &p,
QPoint position,
int count) {
const auto &st = st::emojiPanExpand;
const auto text = u"+%1"_q.arg(count - _columnCount * kCollapsedRows + 1);
const auto textWidth = st.font->width(text);
const auto buttonw = std::max(textWidth - st.width, st.height);
const auto buttonh = st.height;
const auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2;
const auto buttony = position.y() + (_singleSize.height() - buttonh) / 2;
_collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh));
p.setPen(this->st().bg);
p.setFont(st.font);
p.drawText(
buttonx + (buttonw - textWidth) / 2,
(buttony + st.textTop + st.font->ascent),
text);
}
void EmojiListWidget::drawRecent(
QPainter &p,
const ExpandingContext &context,
QPoint position,
const RecentOne &recent) {
_recentPainted = true;
if (const auto custom = recent.custom) {
_emojiPaintContext->scale = context.progress;
_emojiPaintContext->position = position
+ _innerPosition
+ _customPosition;
if (_mode == Mode::ChannelStatus) {
_emojiPaintContext->internal.forceFirstFrame
= (recent.id == _recent.front().id);
}
custom->paint(p, *_emojiPaintContext);
} else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) {
if (_mode == Mode::EmojiStatus) {
position += QPoint(
(_singleSize.width() - st::emojiStatusDefault.width()) / 2,
(_singleSize.height() - st::emojiStatusDefault.height()) / 2
) - _areaPosition;
p.drawImage(position, _premiumIcon->image());
} else {
drawEmoji(p, context, position, *emoji);
}
} else {
Unexpected("Empty custom emoji in EmojiListWidget::drawRecent.");
}
}
void EmojiListWidget::drawEmoji(
QPainter &p,
const ExpandingContext &context,
QPoint position,
EmojiPtr emoji) {
position += _innerPosition;
Ui::Emoji::Draw(
p,
emoji,
Ui::Emoji::GetSizeLarge(),
position.x(),
position.y());
}
void EmojiListWidget::drawCustom(
QPainter &p,
const ExpandingContext &context,
QPoint position,
int set,
int index) {
auto &custom = _custom[set];
custom.painted = true;
auto &entry = custom.list[index];
_emojiPaintContext->scale = context.progress;
_emojiPaintContext->position = position
+ _innerPosition
+ _customPosition;
entry.custom->paint(p, *_emojiPaintContext);
}
bool EmojiListWidget::checkPickerHide() {
if (!_picker->isHidden() && !v::is_null(_pickerSelected)) {
_picker->hideAnimated();
_pickerSelected = v::null;
updateSelected();
return true;
}
return false;
}
DocumentData *EmojiListWidget::lookupCustomEmoji(
const OverEmoji *over) const {
return over ? lookupCustomEmoji(over->index, over->section) : nullptr;
}
DocumentData *EmojiListWidget::lookupCustomEmoji(
int index,
int section) const {
if (_searchMode) {
if (index < _searchResults.size()) {
const auto document = std::get_if<RecentEmojiDocument>(
&_searchResults[index].id.data);
if (document) {
return session().data().document(document->id);
}
}
return nullptr;
} else if (section == int(Section::Recent) && index < _recent.size()) {
const auto document = std::get_if<RecentEmojiDocument>(
&_recent[index].id.data);
if (document) {
return session().data().document(document->id);
}
} else if (section >= _staticCount
&& index < _custom[section - _staticCount].list.size()) {
auto &set = _custom[section - _staticCount];
return set.list[index].document;
}
return nullptr;
}
EmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const {
const auto section = over ? over->section : -1;
const auto index = over ? over->index : -1;
return _searchMode
? ((index < _searchResults.size()
&& v::is<EmojiPtr>(_searchResults[index].id.data))
? v::get<EmojiPtr>(_searchResults[index].id.data)
: nullptr)
: (section == int(Section::Recent)
&& index < _recent.size()
&& v::is<EmojiPtr>(_recent[index].id.data))
? v::get<EmojiPtr>(_recent[index].id.data)
: (section > int(Section::Recent)
&& section < _staticCount
&& index < _emoji[section].size())
? _emoji[section][index]
: nullptr;
}
EmojiChosen EmojiListWidget::lookupChosen(
EmojiPtr emoji,
not_null<const OverEmoji*> over) {
const auto rect = emojiRect(over->section, over->index);
const auto size = st::emojiStatusDefault.size();
const auto icon = QRect(
rect.x() + (_singleSize.width() - size.width()) / 2,
rect.y() + (_singleSize.height() - size.height()) / 2,
rect.width(),
rect.height());
return {
.emoji = emoji,
.messageSendingFrom = {
.type = Ui::MessageSendingAnimationFrom::Type::Emoji,
.globalStartGeometry = mapToGlobal(icon),
},
};
}
FileChosen EmojiListWidget::lookupChosen(
not_null<DocumentData*> custom,
const OverEmoji *over,
Api::SendOptions options) {
_grabbingChosen = true;
const auto guard = gsl::finally([&] { _grabbingChosen = false; });
const auto rect = over ? emojiRect(over->section, over->index) : QRect();
const auto emoji = over ? QRect(
rect.topLeft() + _areaPosition + _innerPosition + _customPosition,
QSize(_customSingleSize, _customSingleSize)
) : QRect();
return {
.document = custom,
.options = options,
.messageSendingFrom = {
.type = Ui::MessageSendingAnimationFrom::Type::Emoji,
.globalStartGeometry = over ? mapToGlobal(emoji) : QRect(),
.frame = over ? Ui::GrabWidgetToImage(this, emoji) : QImage(),
},
};
}
void EmojiListWidget::mousePressEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
updateSelected();
if (checkPickerHide() || e->button() != Qt::LeftButton) {
return;
}
setPressed(_selected);
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
const auto emoji = lookupOverEmoji(over);
if (emoji && emoji->hasVariants()) {
_pickerSelected = _selected;
setCursor(style::cur_default);
if (!Core::App().settings().hasChosenEmojiVariant(emoji)) {
showPicker();
} else {
_previewTimer.cancel();
_showPickerTimer.callOnce(kColorPickerDelay);
}
} else if (lookupCustomEmoji(over)) {
_showPickerTimer.cancel();
_previewTimer.callOnce(QApplication::startDragTime());
}
}
}
void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
_previewTimer.cancel();
auto pressed = _pressed;
setPressed(v::null);
_lastMousePos = e->globalPos();
if (!_picker->isHidden()) {
if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {
return _picker->handleMouseRelease(QCursor::pos());
} else if (const auto over = std::get_if<OverEmoji>(&_pickerSelected)) {
const auto emoji = lookupOverEmoji(over);
if (emoji
&& emoji->hasVariants()
&& Core::App().settings().hasChosenEmojiVariant(emoji)) {
_picker->hideAnimated();
_pickerSelected = v::null;
}
}
}
updateSelected();
if (_showPickerTimer.isActive()) {
_showPickerTimer.cancel();
_pickerSelected = v::null;
_picker->hide();
}
if (_previewShown) {
_previewShown = false;
return;
} else if (v::is_null(_selected) || _selected != pressed) {
return;
}
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
const auto section = over->section;
const auto index = over->index;
if (section >= _staticCount
&& sectionInfo(section).collapsed
&& index + 1 == _columnCount * kCollapsedRows) {
_custom[section - _staticCount].expanded = true;
resizeToWidth(width());
update();
return;
} else if (const auto emoji = lookupOverEmoji(over)) {
if (emoji->hasVariants() && !_picker->isHidden()) {
return;
}
selectEmoji(lookupChosen(emoji, over));
} else if (const auto custom = lookupCustomEmoji(over)) {
selectCustom(lookupChosen(custom, over));
}
} else if (const auto set = std::get_if<OverSet>(&pressed)) {
Assert(set->section >= _staticCount
&& set->section < _staticCount + _custom.size());
displaySet(_custom[set->section - _staticCount].id);
} else if (auto button = std::get_if<OverButton>(&pressed)) {
Assert(hasButton(button->section));
const auto id = hasColorButton(button->section)
? 0
: _custom[button->section - _staticCount].id;
const auto usage = ChatHelpers::WindowUsage::PremiumPromo;
if (hasColorButton(button->section)) {
_pickerSelected = pressed;
showPicker();
} else if (hasRemoveButton(button->section)) {
removeSet(id);
} else if (hasAddButton(button->section)) {
_localSetsManager->install(id);
} else if (const auto resolved = _show->resolveWindow(usage)) {
_jumpedToPremium.fire({});
switch (_mode) {
case Mode::Full:
case Mode::UserpicBuilder:
Settings::ShowPremium(resolved, u"animated_emoji"_q);
break;
case Mode::FullReactions:
case Mode::RecentReactions:
Settings::ShowPremium(resolved, u"infinite_reactions"_q);
break;
case Mode::EmojiStatus:
case Mode::ChannelStatus:
Settings::ShowPremium(resolved, u"emoji_status"_q);
break;
case Mode::TopicIcon:
Settings::ShowPremium(resolved, u"forum_topic_icon"_q);
break;
case Mode::BackgroundEmoji:
Settings::ShowPremium(resolved, u"name_color"_q);
break;
}
}
}
}
void EmojiListWidget::displaySet(uint64 setId) {
if (setId == Data::Stickers::MegagroupSetId) {
if (_megagroupSet->mgInfo->emojiSet.id) {
setId = _megagroupSet->mgInfo->emojiSet.id;
} else {
return;
}
}
const auto &sets = session().data().stickers().sets();
auto it = sets.find(setId);
if (it != sets.cend()) {
checkHideWithBox(Box<StickerSetBox>(_show, it->second.get()));
}
}
void EmojiListWidget::removeMegagroupSet(bool locally) {
if (locally) {
session().settings().setGroupEmojiSectionHidden(_megagroupSet->id);
session().saveSettings();
refreshCustom();
return;
}
checkHideWithBox(Ui::MakeConfirmBox({
.text = tr::lng_emoji_remove_group_set(),
.confirmed = crl::guard(this, [this, group = _megagroupSet](
Fn<void()> &&close) {
Expects(group->mgInfo != nullptr);
if (group->mgInfo->emojiSet) {
session().api().setGroupEmojiSet(group, {});
}
close();
}),
.cancelled = [](Fn<void()> &&close) { close(); },
.labelStyle = &st().boxLabel,
}));
}
void EmojiListWidget::removeSet(uint64 setId) {
const auto &labelSt = st().boxLabel;
if (setId == Data::Stickers::MegagroupSetId) {
const auto i = ranges::find(_custom, setId, &CustomSet::id);
Assert(i != end(_custom));
const auto removeLocally = !_megagroupSet->canEditEmoji();
removeMegagroupSet(removeLocally);
} else if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) {
checkHideWithBox(std::move(box));
}
}
void EmojiListWidget::selectEmoji(EmojiChosen data) {
Core::App().settings().incrementRecentEmoji({ data.emoji });
_chosen.fire(std::move(data));
}
void EmojiListWidget::selectCustom(FileChosen data) {
const auto document = data.document;
const auto skip = (document->isPremiumEmoji() && !session().premium());
if (!skip && _mode == Mode::Full) {
auto &settings = Core::App().settings();
settings.incrementRecentEmoji({ RecentEmojiDocument{
document->id,
document->session().isTestMode(),
} });
}
_customChosen.fire(std::move(data));
}
void EmojiListWidget::showPicker() {
if (v::is_null(_pickerSelected)) {
return;
}
const auto showAt = [&](float64 xCoef, int y, int height) {
y -= _picker->height() - st::emojiPanRadius + getVisibleTop();
if (y < st().header) {
y += _picker->height() + height;
}
auto xmax = width() - _picker->width();
if (rtl()) xCoef = 1. - xCoef;
_picker->move(qRound(xmax * xCoef), y);
disableScroll(true);
};
if (const auto button = std::get_if<OverButton>(&_pickerSelected)) {
const auto hand = QString::fromUtf8("\xF0\x9F\x91\x8B");
const auto emoji = Ui::Emoji::Find(hand);
Assert(emoji != nullptr && emoji->hasVariants());
_picker->showEmoji(emoji, true);
setColorAllForceRippled(true);
const auto rect = buttonRect(button->section);
showAt(1., rect.y(), rect.height() - 2 * st::emojiPanRadius);
} else if (const auto over = std::get_if<OverEmoji>(&_pickerSelected)) {
const auto emoji = lookupOverEmoji(over);
if (emoji && emoji->hasVariants()) {
_picker->showEmoji(emoji);
const auto coef = float64(over->index % _columnCount)
/ float64(_columnCount - 1);
const auto h = _singleSize.height() - 2 * st::emojiPanRadius;
showAt(coef, emojiRect(over->section, over->index).y(), h);
}
}
}
void EmojiListWidget::pickerHidden() {
_pickerSelected = v::null;
update();
disableScroll(false);
setColorAllForceRippled(false);
_lastMousePos = QCursor::pos();
updateSelected();
}
bool EmojiListWidget::hasColorButton(int index) const {
return (_staticCount > int(Section::People))
&& (index == int(Section::People));
}
QRect EmojiListWidget::colorButtonRect(int index) const {
return colorButtonRect(sectionInfo(index));
}
QRect EmojiListWidget::colorButtonRect(const SectionInfo &info) const {
if (_mode != Mode::Full) {
return QRect();
}
const auto &colorSt = st().colorAll;
const auto buttonw = colorSt.rippleAreaPosition.x()
+ colorSt.rippleAreaSize;
const auto buttonh = colorSt.height;
const auto buttonx = emojiRight() - st::emojiPanColorAllSkip - buttonw;
const auto buttony = info.top + st::emojiPanRemoveTop;
return QRect(buttonx, buttony, buttonw, buttonh);
}
bool EmojiListWidget::hasRemoveButton(int index) const {
if (index < _staticCount
|| index >= _staticCount + _custom.size()) {
return false;
}
const auto &set = _custom[index - _staticCount];
if (set.id == Data::Stickers::MegagroupSetId) {
Assert(_megagroupSet != nullptr);
if (index + 1 != _staticCount + _custom.size()) {
return true;
}
return !set.list.empty() && _megagroupSet->canEditEmoji();
}
return set.canRemove && !set.premiumRequired;
}
QRect EmojiListWidget::removeButtonRect(int index) const {
return removeButtonRect(sectionInfo(index));
}
QRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const {
if (_mode != Mode::Full) {
return QRect();
}
const auto &removeSt = st().removeSet;
const auto buttonw = removeSt.rippleAreaPosition.x()
+ removeSt.rippleAreaSize;
const auto buttonh = removeSt.height;
const auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw;
const auto buttony = info.top + st::emojiPanRemoveTop;
return QRect(buttonx, buttony, buttonw, buttonh);
}
bool EmojiListWidget::hasAddButton(int index) const {
if (index < _staticCount
|| index >= _staticCount + _custom.size()) {
return false;
}
const auto &set = _custom[index - _staticCount];
return !set.canRemove
&& !set.premiumRequired
&& set.id != Data::Stickers::MegagroupSetId;
}
QRect EmojiListWidget::addButtonRect(int index) const {
return buttonRect(sectionInfo(index), _add);
}
bool EmojiListWidget::hasUnlockButton(int index) const {
if (index < _staticCount
|| index >= _staticCount + _custom.size()) {
return false;
}
const auto &set = _custom[index - _staticCount];
return set.premiumRequired;
}
QRect EmojiListWidget::unlockButtonRect(int index) const {
Expects(index >= _staticCount
&& index < _staticCount + _custom.size());
return buttonRect(sectionInfo(index), rightButton(index));
}
bool EmojiListWidget::hasButton(int index) const {
if (hasColorButton(index)) {
return true;
} else if (index >= _staticCount
&& index < _staticCount + _custom.size()) {
const auto &custom = _custom[index - _staticCount];
return (custom.id != Data::Stickers::MegagroupSetId)
|| custom.canRemove;
}
return false;
}
QRect EmojiListWidget::buttonRect(int index) const {
return hasColorButton(index)
? colorButtonRect(index)
: hasRemoveButton(index)
? removeButtonRect(index)
: hasAddButton(index)
? addButtonRect(index)
: unlockButtonRect(index);
}
QRect EmojiListWidget::buttonRect(
const SectionInfo &info,
const RightButton &button) const {
const auto buttonw = button.textWidth - st::emojiPanButton.width;
const auto buttonh = st::emojiPanButton.height;
const auto buttonx = emojiRight() - buttonw - st::emojiPanButtonRight;
const auto buttony = info.top + st::emojiPanButtonTop;
return QRect(buttonx, buttony, buttonw, buttonh);
}
auto EmojiListWidget::rightButton(int index) const -> const RightButton & {
Expects(index >= _staticCount
&& index < _staticCount + _custom.size());
return hasAddButton(index)
? _add
: _custom[index - _staticCount].canRemove
? _restore
: _unlock;
}
int EmojiListWidget::emojiRight() const {
return emojiLeft() + (_columnCount * _singleSize.width());
}
int EmojiListWidget::emojiLeft() const {
return _rowsLeft;
}
QRect EmojiListWidget::emojiRect(int section, int index) const {
Expects(_columnCount > 0);
const auto info = sectionInfo(section);
const auto countTillItem = (index - (index % _columnCount));
const auto rowsToSkip = (countTillItem / _columnCount)
+ ((countTillItem % _columnCount) ? 1 : 0);
const auto x = _rowsLeft + ((index % _columnCount) * _singleSize.width());
const auto y = info.rowsTop + rowsToSkip * _singleSize.height();
return QRect(x, y, _singleSize.width(), _singleSize.height());
}
void EmojiListWidget::colorChosen(EmojiChosen data) {
Expects(data.emoji != nullptr && data.emoji->hasVariants());
const auto emoji = data.emoji;
auto &settings = Core::App().settings();
if (const auto button = std::get_if<OverButton>(&_pickerSelected)) {
settings.saveAllEmojiVariants(emoji);
for (auto section = int(Section::People)
; section < _staticCount
; ++section) {
for (auto &emoji : _emoji[section]) {
emoji = settings.lookupEmojiVariant(emoji);
}
}
update();
} else {
settings.saveEmojiVariant(emoji);
const auto over = std::get_if<OverEmoji>(&_pickerSelected);
if (over
&& over->section > int(Section::Recent)
&& over->section < _staticCount
&& over->index < _emoji[over->section].size()) {
_emoji[over->section][over->index] = emoji;
rtlupdate(emojiRect(over->section, over->index));
}
selectEmoji(data);
}
_picker->hideAnimated();
}
void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
if (!_picker->isHidden()) {
if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {
return _picker->handleMouseMove(QCursor::pos());
} else {
_picker->clearSelection();
}
}
updateSelected();
}
void EmojiListWidget::leaveEventHook(QEvent *e) {
clearSelection();
}
void EmojiListWidget::leaveToChildEvent(QEvent *e, QWidget *child) {
clearSelection();
}
void EmojiListWidget::enterFromChildEvent(QEvent *e, QWidget *child) {
_lastMousePos = QCursor::pos();
updateSelected();
}
void EmojiListWidget::clearSelection() {
setPressed(v::null);
setSelected(v::null);
_lastMousePos = mapToGlobal(QPoint(-10, -10));
}
uint64 EmojiListWidget::currentSet(int yOffset) const {
return sectionSetId(sectionInfoByOffset(yOffset).section);
}
void EmojiListWidget::setAllowWithoutPremium(bool allow) {
if (_allowWithoutPremium == allow) {
return;
}
_allowWithoutPremium = allow;
refreshCustom();
resizeToWidth(width());
}
void EmojiListWidget::showMegagroupSet(ChannelData *megagroup) {
Expects(!megagroup || megagroup->isMegagroup());
if (_megagroupSet != megagroup) {
_megagroupSet = megagroup;
refreshCustom();
resizeToWidth(width());
}
}
QString EmojiListWidget::tooltipText() const {
if (_mode != Mode::Full) {
return {};
}
const auto &replacements = Ui::Emoji::internal::GetAllReplacements();
const auto over = std::get_if<OverEmoji>(&_selected);
if (const auto emoji = lookupOverEmoji(over)) {
const auto text = emoji->original()->text();
// find the replacement belonging to the emoji
const auto it = ranges::find_if(replacements, [&](const auto &one) {
return text == Ui::Emoji::QStringFromUTF16(one.emoji);
});
if (it != replacements.end()) {
return Ui::Emoji::QStringFromUTF16(it->replacement);
}
}
return {};
}
QPoint EmojiListWidget::tooltipPos() const {
return _lastMousePos;
}
bool EmojiListWidget::tooltipWindowActive() const {
return Ui::AppInFocus() && Ui::InFocusChain(window());
}
TabbedSelector::InnerFooter *EmojiListWidget::getFooter() const {
return _footer;
}
void EmojiListWidget::processHideFinished() {
if (!_picker->isHidden()) {
_picker->hideFast();
_pickerSelected = v::null;
}
unloadAllCustom();
clearSelection();
}
void EmojiListWidget::processPanelHideFinished() {
unloadAllCustom();
if (_localSetsManager->clearInstalledLocally()) {
refreshCustom();
}
}
void EmojiListWidget::refreshRecent() {
if (_mode != Mode::Full) {
return;
}
clearSelection();
fillRecent();
resizeToWidth(width());
update();
}
void EmojiListWidget::refreshCustom() {
if (_mode == Mode::RecentReactions) {
return;
}
auto old = base::take(_custom);
const auto session = &this->session();
const auto premiumPossible = session->premiumPossible();
const auto onlyUnicodeEmoji = _onlyUnicodeEmoji || !premiumPossible;
const auto premiumMayBeBought = (!onlyUnicodeEmoji)
&& premiumPossible
&& !session->premium()
&& !_allowWithoutPremium;
const auto owner = &session->data();
const auto &sets = owner->stickers().sets();
const auto push = [&](uint64 setId, bool installed) {
const auto megagroup = _megagroupSet
&& (setId == Data::Stickers::MegagroupSetId);
const auto lookupId = megagroup
? _megagroupSet->mgInfo->emojiSet.id
: setId;
if (!lookupId) {
return;
} else if (!megagroup
&& !_custom.empty()
&& _custom.front().id == Data::Stickers::MegagroupSetId
&& _megagroupSet->mgInfo->emojiSet.id == setId) {
// Skip the set that is already added as a megagroup set.
return;
} else if (megagroup
&& ranges::contains(_custom, lookupId, &CustomSet::id)) {
// Skip the set that is already added as a custom set.
return;
}
auto it = sets.find(lookupId);
if (it == sets.cend()
|| it->second->stickers.isEmpty()
|| (_mode == Mode::BackgroundEmoji && !it->second->textColor())
|| (_mode == Mode::ChannelStatus
&& !it->second->channelStatus())) {
return;
}
const auto canRemove = megagroup
? (_megagroupSet->canEditEmoji() || installed)
: !!(it->second->flags & Data::StickersSetFlag::Installed);
const auto sortAsInstalled = canRemove
&& (!(it->second->flags & Data::StickersSetFlag::Featured)
|| !_localSetsManager->isInstalledLocally(lookupId));
if (!megagroup && sortAsInstalled != installed) {
return;
}
auto premium = false;
const auto &list = it->second->stickers;
const auto i = ranges::find(old, setId, &CustomSet::id);
if (i != end(old)) {
const auto valid = [&] {
const auto count = int(list.size());
if (i->list.size() != count) {
return false;
}
for (auto k = 0; k != count; ++k) {
if (!premium && !megagroup && list[k]->isPremiumEmoji()) {
premium = true;
}
if (i->list[k].document != list[k]) {
return false;
}
}
return true;
}();
if (premium && onlyUnicodeEmoji) {
return;
} else if (valid) {
i->thumbnailDocument = it->second->lookupThumbnailDocument();
const auto premiumRequired = premium && premiumMayBeBought;
if (i->canRemove != canRemove
|| i->premiumRequired != premiumRequired) {
i->canRemove = canRemove;
i->premiumRequired = premiumRequired;
i->ripple.reset();
}
if (i->canRemove && !i->premiumRequired) {
i->expanded = false;
}
_custom.push_back(std::move(*i));
return;
}
}
auto set = std::vector<CustomOne>();
set.reserve(list.size());
for (const auto document : list) {
if (_restrictedCustomList.contains(document->id)) {
continue;
} else if (const auto sticker = document->sticker()) {
set.push_back({
.custom = resolveCustomEmoji(document, lookupId),
.document = document,
.emoji = Ui::Emoji::Find(sticker->alt),
});
if (!premium && !megagroup && document->isPremiumEmoji()) {
premium = true;
}
}
}
if (premium && onlyUnicodeEmoji) {
return;
}
_custom.push_back({
.id = setId,
.set = it->second.get(),
.thumbnailDocument = it->second->lookupThumbnailDocument(),
.title = it->second->title,
.list = std::move(set),
.canRemove = canRemove,
.premiumRequired = premium && premiumMayBeBought,
});
};
refreshMegagroupStickers(push, GroupStickersPlace::Visible);
for (const auto setId : owner->stickers().emojiSetsOrder()) {
push(setId, true);
}
for (const auto setId : owner->stickers().featuredEmojiSetsOrder()) {
push(setId, false);
}
refreshMegagroupStickers(push, GroupStickersPlace::Hidden);
_footer->refreshIcons(
fillIcons(),
currentSet(getVisibleTop()),
nullptr,
ValidateIconAnimations::None);
update();
}
Fn<void()> EmojiListWidget::repaintCallback(
DocumentId documentId,
uint64 setId) {
return [=] {
repaintCustom(setId);
if (_recentCustomIds.contains(documentId)) {
repaintCustom(RecentEmojiSectionSetId());
}
if (_searchCustomIds.contains(documentId)) {
repaintCustom(SearchEmojiSectionSetId());
}
};
}
not_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomEmoji(
not_null<DocumentData*> document,
uint64 setId) {
Expects(document->sticker() != nullptr);
const auto documentId = document->id;
const auto i = _customEmoji.find(documentId);
const auto recentOnly = (i != end(_customEmoji)) && i->second.recentOnly;
if (i != end(_customEmoji) && !recentOnly) {
return i->second.emoji.get();
}
auto instance = document->owner().customEmojiManager().create(
document,
repaintCallback(documentId, setId),
Data::CustomEmojiManager::SizeTag::Large);
if (recentOnly) {
for (auto &recent : _recent) {
if (recent.custom && recent.custom == i->second.emoji.get()) {
recent.custom = instance.get();
}
}
i->second.emoji = std::move(instance);
i->second.recentOnly = false;
return i->second.emoji.get();
}
return _customEmoji.emplace(
documentId,
CustomEmojiInstance{ .emoji = std::move(instance) }
).first->second.emoji.get();
}
Ui::Text::CustomEmoji *EmojiListWidget::resolveCustomRecent(
RecentEmojiId customId) {
const auto &data = customId.data;
if (const auto document = std::get_if<RecentEmojiDocument>(&data)) {
return resolveCustomRecent(document->id);
} else if (const auto emoji = std::get_if<EmojiPtr>(&data)) {
return nullptr;
}
Unexpected("Custom recent emoji id.");
}
not_null<Ui::Text::CustomEmoji*> EmojiListWidget::resolveCustomRecent(
DocumentId documentId) {
const auto i = _customRecent.find(documentId);
if (i != end(_customRecent)) {
return i->second.get();
}
const auto j = _customEmoji.find(documentId);
if (j != end(_customEmoji)) {
return j->second.emoji.get();
}
auto repaint = repaintCallback(documentId, RecentEmojiSectionSetId());
if (_customRecentFactory) {
return _customRecent.emplace(
documentId,
_customRecentFactory(documentId, std::move(repaint))
).first->second.get();
}
auto custom = session().data().customEmojiManager().create(
documentId,
std::move(repaint),
Data::CustomEmojiManager::SizeTag::Large);
return _customEmoji.emplace(
documentId,
CustomEmojiInstance{ .emoji = std::move(custom), .recentOnly = true }
).first->second.emoji.get();
}
void EmojiListWidget::refreshMegagroupStickers(
Fn<void(uint64 setId, bool installed)> push,
GroupStickersPlace place) {
if (!_features.megagroupSet
|| !_megagroupSet
|| !_megagroupSet->mgInfo->emojiSet) {
return;
}
auto canEdit = _megagroupSet->canEditEmoji();
auto isShownHere = [place](bool hidden) {
return (hidden == (place == GroupStickersPlace::Hidden));
};
auto hidden = session().settings().isGroupEmojiSectionHidden(_megagroupSet->id);
auto removeHiddenForGroup = [this, &hidden] {
if (hidden) {
session().settings().removeGroupEmojiSectionHidden(_megagroupSet->id);
session().saveSettings();
hidden = false;
}
};
if (canEdit && hidden) {
removeHiddenForGroup();
}
const auto &set = _megagroupSet->mgInfo->emojiSet;
if (!set.id || !isShownHere(hidden)) {
return;
}
push(Data::Stickers::MegagroupSetId, !hidden);
if (!_custom.empty()
&& _custom.back().id == Data::Stickers::MegagroupSetId) {
return;
} else if (_megagroupSetIdRequested == set.id) {
return;
}
_megagroupSetIdRequested = set.id;
_api.request(MTPmessages_GetStickerSet(
Data::InputStickerSet(set),
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &data) {
if (const auto set = session().data().stickers().feedSetFull(data)) {
refreshCustom();
if (set->id == _megagroupSetIdRequested) {
_megagroupSetIdRequested = 0;
} else {
LOG(("API Error: Got different set."));
}
}
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).send();
}
std::vector<StickerIcon> EmojiListWidget::fillIcons() {
auto result = std::vector<StickerIcon>();
result.reserve(2 + _custom.size());
result.emplace_back(RecentEmojiSectionSetId());
if (_mode != Mode::Full) {
} else if (_custom.empty()) {
using Section = Ui::Emoji::Section;
for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {
result.emplace_back(EmojiSectionSetId(Section(i)));
}
} else {
result.emplace_back(AllEmojiSectionSetId());
}
const auto esize = StickersListFooter::IconFrameSize();
for (const auto &custom : _custom) {
if (custom.id == Data::Stickers::MegagroupSetId) {
result.emplace_back(Data::Stickers::MegagroupSetId);
result.back().megagroup = _megagroupSet;
continue;
}
const auto set = custom.set;
result.emplace_back(set, custom.thumbnailDocument, esize, esize);
}
return result;
}
int EmojiListWidget::paintButtonGetWidth(
QPainter &p,
const SectionInfo &info,
bool selected,
QRect clip) const {
if (!hasButton(info.section)) {
return 0;
}
auto &ripple = (info.section >= _staticCount)
? _custom[info.section - _staticCount].ripple
: _colorAllRipple;
const auto colorAll = hasColorButton(info.section);
if (colorAll || hasRemoveButton(info.section)) {
const auto rect = colorAll
? colorButtonRect(info)
: removeButtonRect(info);
if (rect.isEmpty()) {
return 0;
} else if (rect.intersects(clip)) {
const auto &bst = colorAll ? st().colorAll : st().removeSet;
if (colorAll && _colorAllRippleForced) {
selected = true;
}
if (ripple) {
ripple->paint(
p,
rect.x() + bst.rippleAreaPosition.x(),
rect.y() + bst.rippleAreaPosition.y(),
width());
if (ripple->empty()) {
ripple.reset();
}
}
const auto &icon = selected ? bst.iconOver : bst.icon;
icon.paint(
p,
(rect.topLeft()
+ QPoint(
rect.width() - icon.width(),
rect.height() - icon.height()) / 2),
width());
}
return emojiRight() - rect.x();
}
const auto canAdd = hasAddButton(info.section);
const auto &button = rightButton(info.section);
const auto rect = buttonRect(info, button);
p.drawImage(rect.topLeft(), selected ? button.backOver : button.back);
if (ripple) {
const auto color = QColor(0, 0, 0, 36);
ripple->paint(p, rect.x(), rect.y(), width(), &color);
if (ripple->empty()) {
ripple.reset();
}
}
p.setPen(!canAdd
? st::premiumButtonFg
: selected
? st::emojiPanButton.textFgOver
: st::emojiPanButton.textFg);
p.setFont(st::emojiPanButton.font);
p.drawText(
rect.x() - (st::emojiPanButton.width / 2),
(rect.y()
+ st::emojiPanButton.textTop
+ st::emojiPanButton.font->ascent),
button.text);
return emojiRight() - rect.x();
}
void EmojiListWidget::paintEmptySearchResults(Painter &p) {
Inner::paintEmptySearchResults(
p,
st::emojiEmpty,
tr::lng_emoji_nothing_found(tr::now));
}
bool EmojiListWidget::eventHook(QEvent *e) {
if (e->type() == QEvent::ParentChange) {
if (_picker->parentWidget() != parentWidget()) {
_picker->setParent(parentWidget());
}
_picker->raise();
}
return Inner::eventHook(e);
}
void EmojiListWidget::updateSelected() {
if (!v::is_null(_pressed) || !v::is_null(_pickerSelected)) {
if (!_previewShown) {
return;
}
}
auto newSelected = OverState{ v::null };
auto p = mapFromGlobal(_lastMousePos);
auto info = sectionInfoByOffset(p.y());
auto section = info.section;
if (p.y() >= info.top && p.y() < info.rowsTop) {
if (hasButton(section)
&& myrtlrect(buttonRect(section)).contains(p.x(), p.y())) {
newSelected = OverButton{ section };
} else if (_features.openStickerSets
&& section >= _staticCount
&& _mode == Mode::Full) {
newSelected = OverSet{ section };
}
} else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) {
auto sx = (rtl() ? width() - p.x() : p.x()) - _rowsLeft;
if (sx >= 0 && sx < _columnCount * _singleSize.width()) {
const auto index = qFloor((p.y() - info.rowsTop) / _singleSize.height()) * _columnCount + qFloor(sx / _singleSize.width());
if (index < info.count) {
newSelected = OverEmoji{ .section = section, .index = index };
}
}
}
setSelected(newSelected);
}
void EmojiListWidget::setSelected(OverState newSelected) {
if (_selected == newSelected) {
return;
}
setCursor(!v::is_null(newSelected)
? style::cur_pointer
: style::cur_default);
const auto updateSelected = [&] {
if (const auto sticker = std::get_if<OverEmoji>(&_selected)) {
rtlupdate(emojiRect(sticker->section, sticker->index));
} else if (const auto button = std::get_if<OverButton>(&_selected)) {
rtlupdate(buttonRect(button->section));
}
};
updateSelected();
_selected = newSelected;
updateSelected();
const auto hasSelection = !v::is_null(_selected);
if (hasSelection && Core::App().settings().suggestEmoji()) {
Ui::Tooltip::Show(1000, this);
}
setCursor(hasSelection ? style::cur_pointer : style::cur_default);
if (hasSelection && !_picker->isHidden()) {
if (_selected != _pickerSelected) {
_picker->hideAnimated();
} else {
_picker->showAnimated();
}
} else if (_previewShown && _pressed != _selected) {
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
if (const auto custom = lookupCustomEmoji(over)) {
_pressed = _selected;
_show->showMediaPreview(custom->stickerSetOrigin(), custom);
}
}
}
}
void EmojiListWidget::setPressed(OverState newPressed) {
if (auto button = std::get_if<OverButton>(&_pressed)) {
Assert(hasColorButton(button->section)
|| (button->section >= _staticCount
&& button->section < _staticCount + _custom.size()));
auto &ripple = (button->section >= _staticCount)
? _custom[button->section - _staticCount].ripple
: _colorAllRipple;
if (ripple) {
ripple->lastStop();
}
}
_pressed = newPressed;
if (auto button = std::get_if<OverButton>(&_pressed)) {
Assert(hasColorButton(button->section)
|| (button->section >= _staticCount
&& button->section < _staticCount + _custom.size()));
auto &ripple = (button->section >= _staticCount)
? _custom[button->section - _staticCount].ripple
: _colorAllRipple;
if (!ripple) {
ripple = createButtonRipple(button->section);
}
ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section));
}
}
void EmojiListWidget::initButton(
RightButton &button,
const QString &text,
bool gradient) {
button.text = text;
button.textWidth = st::emojiPanButton.font->width(text);
const auto width = button.textWidth - st::emojiPanButton.width;
const auto height = st::emojiPanButton.height;
const auto factor = style::DevicePixelRatio();
auto prepare = [&](QColor bg, QBrush fg) {
auto image = QImage(
QSize(width, height) * factor,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
auto p = QPainter(&image);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(fg);
const auto radius = height / 2.;
p.drawRoundedRect(QRect(0, 0, width, height), radius, radius);
p.end();
return image;
};
button.back = prepare(Qt::transparent, [&]() -> QBrush {
if (gradient) {
auto result = QLinearGradient(QPointF(0, 0), QPointF(width, 0));
result.setStops(Ui::Premium::GiftGradientStops());
return result;
}
return st::emojiPanButton.textBg;
}());
button.backOver = gradient
? button.back
: prepare(Qt::transparent, st::emojiPanButton.textBgOver);
button.rippleMask = prepare(Qt::black, Qt::white);
}
std::unique_ptr<Ui::RippleAnimation> EmojiListWidget::createButtonRipple(
int section) {
Expects(hasButton(section));
const auto colorAll = hasColorButton(section);
const auto remove = hasRemoveButton(section);
const auto &staticSt = colorAll ? st().colorAll : st().removeSet;
const auto &st = (colorAll || remove)
? staticSt.ripple
: st::emojiPanButton.ripple;
auto mask = (colorAll || remove)
? Ui::RippleAnimation::EllipseMask(QSize(
staticSt.rippleAreaSize,
staticSt.rippleAreaSize))
: rightButton(section).rippleMask;
return std::make_unique<Ui::RippleAnimation>(
st,
std::move(mask),
[this, section] { rtlupdate(buttonRect(section)); });
}
QPoint EmojiListWidget::buttonRippleTopLeft(int section) const {
Expects(hasButton(section));
return myrtlrect(buttonRect(section)).topLeft()
+ (hasColorButton(section)
? st().colorAll.rippleAreaPosition
: hasRemoveButton(section)
? st().removeSet.rippleAreaPosition
: QPoint());
}
PowerSaving::Flag EmojiListWidget::powerSavingFlag() const {
const auto reactions = (_mode == Mode::FullReactions)
|| (_mode == Mode::RecentReactions);
return reactions
? PowerSaving::kEmojiReactions
: PowerSaving::kEmojiPanel;
}
void EmojiListWidget::refreshEmoji() {
refreshRecent();
refreshCustom();
}
void EmojiListWidget::showSet(uint64 setId) {
clearSelection();
if (_search && _searchMode) {
_search->cancel();
applyNextSearchQuery();
}
auto y = 0;
enumerateSections([&](const SectionInfo &info) {
if (setId == sectionSetId(info.section)) {
y = info.top;
return false;
}
return true;
});
scrollTo(y);
_lastMousePos = QCursor::pos();
update();
}
uint64 EmojiListWidget::sectionSetId(int section) const {
Expects(_searchMode
|| section < _staticCount
|| (section - _staticCount) < _custom.size());
return _searchMode
? SearchEmojiSectionSetId()
: (section < _staticCount)
? EmojiSectionSetId(static_cast<Section>(section))
: _custom[section - _staticCount].id;
}
tr::phrase<> EmojiCategoryTitle(int index) {
switch (index) {
case 1: return tr::lng_emoji_category1;
case 2: return tr::lng_emoji_category2;
case 3: return tr::lng_emoji_category3;
case 4: return tr::lng_emoji_category4;
case 5: return tr::lng_emoji_category5;
case 6: return tr::lng_emoji_category6;
case 7: return tr::lng_emoji_category7;
}
Unexpected("Index in CategoryTitle.");
}
} // namespace ChatHelpers