1138 lines
32 KiB
C++
1138 lines
32 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 "history/view/reactions/history_view_reactions_selector.h"
|
|
|
|
#include "ui/widgets/scroll_area.h"
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "ui/widgets/shadow.h"
|
|
#include "ui/text/text_custom_emoji.h"
|
|
#include "ui/platform/ui_platform_utility.h"
|
|
#include "ui/painter.h"
|
|
#include "history/history_item.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_session.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "main/main_session.h"
|
|
#include "chat_helpers/emoji_list_widget.h"
|
|
#include "chat_helpers/stickers_list_footer.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "boxes/premium_preview_box.h"
|
|
#include "mainwidget.h"
|
|
#include "apiwrap.h"
|
|
#include "base/call_delayed.h"
|
|
#include "styles/style_chat_helpers.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace HistoryView::Reactions {
|
|
namespace {
|
|
|
|
constexpr auto kExpandDuration = crl::time(300);
|
|
constexpr auto kScaleDuration = crl::time(120);
|
|
constexpr auto kFullDuration = kExpandDuration + kScaleDuration;
|
|
constexpr auto kExpandDelay = crl::time(40);
|
|
constexpr auto kDefaultColumns = 8;
|
|
constexpr auto kMinNonTransparentColumns = 7;
|
|
|
|
class StripEmoji final : public Ui::Text::CustomEmoji {
|
|
public:
|
|
StripEmoji(
|
|
std::unique_ptr<Ui::Text::CustomEmoji> wrapped,
|
|
not_null<Strip*> strip,
|
|
QPoint shift,
|
|
int index);
|
|
|
|
QString entityData() override;
|
|
void paint(QPainter &p, const Context &context) override;
|
|
void unload() override;
|
|
bool ready() override;
|
|
bool readyInDefaultState() override;
|
|
|
|
private:
|
|
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
|
const not_null<Strip*> _strip;
|
|
const QPoint _shift;
|
|
const int _index = 0;
|
|
bool _switched = false;
|
|
|
|
};
|
|
|
|
StripEmoji::StripEmoji(
|
|
std::unique_ptr<Ui::Text::CustomEmoji> wrapped,
|
|
not_null<Strip*> strip,
|
|
QPoint shift,
|
|
int index)
|
|
: _wrapped(std::move(wrapped))
|
|
, _strip(strip)
|
|
, _shift(shift)
|
|
, _index(index) {
|
|
}
|
|
|
|
QString StripEmoji::entityData() {
|
|
return _wrapped->entityData();
|
|
}
|
|
|
|
void StripEmoji::paint(QPainter &p, const Context &context) {
|
|
if (_switched) {
|
|
_wrapped->paint(p, context);
|
|
} else if (_wrapped->readyInDefaultState()
|
|
&& _strip->inDefaultState(_index)) {
|
|
_switched = true;
|
|
_wrapped->paint(p, context);
|
|
} else {
|
|
_strip->paintOne(p, _index, context.position + _shift, 1.);
|
|
}
|
|
}
|
|
|
|
void StripEmoji::unload() {
|
|
_wrapped->unload();
|
|
_switched = true;
|
|
}
|
|
|
|
bool StripEmoji::ready() {
|
|
return _wrapped->ready();
|
|
}
|
|
|
|
bool StripEmoji::readyInDefaultState() {
|
|
return _wrapped->readyInDefaultState();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Selector::Selector(
|
|
not_null<QWidget*> parent,
|
|
const style::EmojiPan &st,
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
const Data::PossibleItemReactionsRef &reactions,
|
|
IconFactory iconFactory,
|
|
Fn<void(bool fast)> close,
|
|
bool child)
|
|
: Selector(
|
|
parent,
|
|
st,
|
|
std::move(show),
|
|
reactions,
|
|
(reactions.customAllowed
|
|
? ChatHelpers::EmojiListMode::FullReactions
|
|
: ChatHelpers::EmojiListMode::RecentReactions),
|
|
{},
|
|
iconFactory,
|
|
close,
|
|
child) {
|
|
}
|
|
|
|
Selector::Selector(
|
|
not_null<QWidget*> parent,
|
|
const style::EmojiPan &st,
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
ChatHelpers::EmojiListMode mode,
|
|
std::vector<DocumentId> recent,
|
|
Fn<void(bool fast)> close,
|
|
bool child)
|
|
: Selector(
|
|
parent,
|
|
st,
|
|
std::move(show),
|
|
{ .customAllowed = true },
|
|
mode,
|
|
std::move(recent),
|
|
nullptr,
|
|
close,
|
|
child) {
|
|
}
|
|
|
|
Selector::Selector(
|
|
not_null<QWidget*> parent,
|
|
const style::EmojiPan &st,
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
const Data::PossibleItemReactionsRef &reactions,
|
|
ChatHelpers::EmojiListMode mode,
|
|
std::vector<DocumentId> recent,
|
|
IconFactory iconFactory,
|
|
Fn<void(bool fast)> close,
|
|
bool child)
|
|
: RpWidget(parent)
|
|
, _st(st)
|
|
, _show(std::move(show))
|
|
, _reactions(reactions)
|
|
, _recent(std::move(recent))
|
|
, _listMode(mode)
|
|
, _jumpedToPremium([=] { close(false); })
|
|
, _cachedRound(
|
|
QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight),
|
|
st::reactionCornerShadow,
|
|
st::reactStripHeight)
|
|
, _strip(iconFactory
|
|
? std::make_unique<Strip>(
|
|
_st,
|
|
QRect(0, 0, st::reactStripSize, st::reactStripSize),
|
|
st::reactStripImage,
|
|
crl::guard(this, [=] { update(_inner); }),
|
|
std::move(iconFactory))
|
|
: nullptr)
|
|
, _size(st::reactStripSize)
|
|
, _skipx(countSkipLeft())
|
|
, _skipy((st::reactStripHeight - st::reactStripSize) / 2) {
|
|
setMouseTracking(true);
|
|
|
|
_useTransparency = child || Ui::Platform::TranslucentWindowsSupported();
|
|
}
|
|
|
|
bool Selector::useTransparency() const {
|
|
return _useTransparency;
|
|
}
|
|
|
|
int Selector::recentCount() const {
|
|
return int(_strip ? _reactions.recent.size() : _recent.size());
|
|
}
|
|
|
|
int Selector::countSkipLeft() const {
|
|
const auto addedToMax = _reactions.customAllowed
|
|
|| _reactions.morePremiumAvailable;
|
|
const auto max = recentCount() + (addedToMax ? 1 : 0);
|
|
return std::max(
|
|
(st::reactStripMinWidth - (max * _size)) / 2,
|
|
st::reactStripSkip);
|
|
}
|
|
|
|
int Selector::countWidth(int desiredWidth, int maxWidth) {
|
|
const auto addedToMax = _reactions.customAllowed
|
|
|| _reactions.morePremiumAvailable;
|
|
const auto max = recentCount() + (addedToMax ? 1 : 0);
|
|
const auto desiredColumns = std::max(
|
|
(desiredWidth - 2 * _skipx + _size - 1) / _size,
|
|
kMinNonTransparentColumns);
|
|
const auto possibleColumns = std::min(
|
|
desiredColumns,
|
|
(maxWidth - 2 * _skipx) / _size);
|
|
_columns = _strip ? std::min(possibleColumns, max) : kDefaultColumns;
|
|
_small = (possibleColumns - _columns > 1);
|
|
_recentRows = (recentCount()
|
|
+ (_reactions.morePremiumAvailable ? 1 : 0)
|
|
+ _columns - 1) / _columns;
|
|
const auto added = (_columns < max || _reactions.customAllowed)
|
|
? Strip::AddedButton::Expand
|
|
: _reactions.morePremiumAvailable
|
|
? Strip::AddedButton::Premium
|
|
: Strip::AddedButton::None;
|
|
if (_strip) {
|
|
const auto &real = _reactions.recent;
|
|
auto list = std::vector<not_null<const Data::Reaction*>>();
|
|
list.reserve(_columns);
|
|
if (const auto cut = max - _columns) {
|
|
const auto from = begin(real);
|
|
const auto till = end(real) - (cut + (addedToMax ? 0 : 1));
|
|
for (auto i = from; i != till; ++i) {
|
|
list.push_back(&*i);
|
|
}
|
|
} else {
|
|
for (const auto &reaction : real) {
|
|
list.push_back(&reaction);
|
|
}
|
|
}
|
|
_strip->applyList(list, added);
|
|
_strip->clearAppearAnimations(false);
|
|
}
|
|
return std::max(2 * _skipx + _columns * _size, desiredWidth);
|
|
}
|
|
|
|
QMargins Selector::extentsForShadow() const {
|
|
const auto line = st::lineWidth;
|
|
return useTransparency()
|
|
? st::reactionCornerShadow
|
|
: QMargins(line, line, line, line);
|
|
}
|
|
|
|
int Selector::extendTopForCategories() const {
|
|
return _reactions.customAllowed ? _st.footer : 0;
|
|
}
|
|
|
|
int Selector::minimalHeight() const {
|
|
return _skipy
|
|
+ (_recentRows * _size)
|
|
+ st::emojiPanRadius
|
|
+ _st.padding.bottom();
|
|
}
|
|
|
|
void Selector::setSpecialExpandTopSkip(int skip) {
|
|
_specialExpandTopSkip = skip;
|
|
}
|
|
|
|
void Selector::initGeometry(int innerTop) {
|
|
const auto extents = extentsForShadow();
|
|
const auto parent = parentWidget()->rect();
|
|
const auto innerWidth = 2 * _skipx + _columns * _size;
|
|
const auto innerHeight = st::reactStripHeight;
|
|
const auto width = _useTransparency
|
|
? (innerWidth + extents.left() + extents.right())
|
|
: parent.width();
|
|
const auto height = innerHeight + extents.top() + extents.bottom();
|
|
const auto left = style::RightToLeft() ? 0 : (parent.width() - width);
|
|
_collapsedTopSkip = _useTransparency
|
|
? (extendTopForCategories() + _specialExpandTopSkip)
|
|
: 0;
|
|
const auto top = innerTop - extents.top() - _collapsedTopSkip;
|
|
const auto add = _st.icons.stripBubble.height() - extents.bottom();
|
|
_outer = QRect(0, _collapsedTopSkip, width, height);
|
|
_outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add });
|
|
setGeometry(_outerWithBubble.marginsAdded(
|
|
{ 0, _collapsedTopSkip, 0, 0 }
|
|
).translated(left, top));
|
|
_inner = _outer.marginsRemoved(extents);
|
|
|
|
if (!_strip) {
|
|
expand();
|
|
}
|
|
}
|
|
|
|
void Selector::beforeDestroy() {
|
|
if (_list) {
|
|
_list->beforeHiding();
|
|
}
|
|
}
|
|
|
|
rpl::producer<> Selector::escapes() const {
|
|
return _escapes.events();
|
|
}
|
|
|
|
void Selector::updateShowState(
|
|
float64 progress,
|
|
float64 opacity,
|
|
bool appearing,
|
|
bool toggling) {
|
|
if (_useTransparency
|
|
&& _appearing
|
|
&& !appearing
|
|
&& !_paintBuffer.isNull()) {
|
|
paintBackgroundToBuffer();
|
|
}
|
|
_appearing = appearing;
|
|
_toggling = toggling;
|
|
_appearProgress = progress;
|
|
_appearOpacity = opacity;
|
|
if (_appearing && isHidden()) {
|
|
show();
|
|
raise();
|
|
} else if (_toggling && !isHidden()) {
|
|
hide();
|
|
}
|
|
if (!_appearing && !_low) {
|
|
_low = true;
|
|
lower();
|
|
}
|
|
update();
|
|
}
|
|
|
|
int Selector::countAppearedWidth(float64 progress) const {
|
|
return anim::interpolate(_skipx * 2 + _size, _inner.width(), progress);
|
|
}
|
|
|
|
void Selector::paintAppearing(QPainter &p) {
|
|
Expects(_strip != nullptr);
|
|
|
|
p.setOpacity(_appearOpacity);
|
|
|
|
const auto factor = style::DevicePixelRatio();
|
|
if (_paintBuffer.size() != _outerWithBubble.size() * factor) {
|
|
_paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size());
|
|
}
|
|
_paintBuffer.fill(_st.bg->c);
|
|
auto q = QPainter(&_paintBuffer);
|
|
const auto extents = extentsForShadow();
|
|
const auto appearedWidth = countAppearedWidth(_appearProgress);
|
|
const auto fullWidth = _inner.x() + appearedWidth + extents.right();
|
|
const auto size = QSize(fullWidth, _outer.height());
|
|
|
|
q.translate(_inner.topLeft() - QPoint(0, _collapsedTopSkip));
|
|
_strip->paint(
|
|
q,
|
|
{ _skipx, _skipy },
|
|
{ _size, 0 },
|
|
{ 0, 0, appearedWidth, _inner.height() },
|
|
1.,
|
|
false);
|
|
|
|
_cachedRound.setBackgroundColor(_st.bg->c);
|
|
_cachedRound.setShadowColor(st::shadowFg->c);
|
|
q.translate(QPoint(0, _collapsedTopSkip) - _inner.topLeft());
|
|
const auto radius = st::reactStripHeight / 2;
|
|
_cachedRound.overlayExpandedBorder(
|
|
q,
|
|
size,
|
|
_appearProgress,
|
|
radius,
|
|
radius,
|
|
1.);
|
|
q.setCompositionMode(QPainter::CompositionMode_Source);
|
|
q.fillRect(
|
|
QRect{ 0, size.height(), width(), height() - size.height() },
|
|
Qt::transparent);
|
|
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
paintBubble(q, appearedWidth);
|
|
q.end();
|
|
|
|
p.drawImage(
|
|
_outer.topLeft(),
|
|
_paintBuffer,
|
|
QRect(QPoint(), QSize(fullWidth, height()) * factor));
|
|
}
|
|
|
|
void Selector::paintBackgroundToBuffer() {
|
|
if (!_useTransparency) {
|
|
return;
|
|
}
|
|
const auto factor = style::DevicePixelRatio();
|
|
if (_paintBuffer.size() != _outerWithBubble.size() * factor) {
|
|
_paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size());
|
|
}
|
|
_paintBuffer.fill(Qt::transparent);
|
|
|
|
_cachedRound.setBackgroundColor(_st.bg->c);
|
|
_cachedRound.setShadowColor(st::shadowFg->c);
|
|
|
|
auto p = QPainter(&_paintBuffer);
|
|
const auto radius = _inner.height() / 2.;
|
|
const auto frame = _cachedRound.validateFrame(0, 1., radius);
|
|
const auto outer = _outer.translated(0, -_collapsedTopSkip);
|
|
_cachedRound.FillWithImage(p, outer, frame);
|
|
paintBubble(p, _inner.width());
|
|
}
|
|
|
|
void Selector::paintCollapsed(QPainter &p) {
|
|
Expects(_strip != nullptr);
|
|
|
|
if (_useTransparency) {
|
|
if (_paintBuffer.isNull()) {
|
|
paintBackgroundToBuffer();
|
|
}
|
|
p.drawImage(_outer.topLeft(), _paintBuffer);
|
|
} else {
|
|
p.fillRect(_inner, _st.bg);
|
|
}
|
|
_strip->paint(
|
|
p,
|
|
_inner.topLeft() + QPoint(_skipx, _skipy),
|
|
{ _size, 0 },
|
|
_inner,
|
|
1.,
|
|
false);
|
|
}
|
|
|
|
void Selector::paintExpanding(Painter &p, float64 progress) {
|
|
const auto rects = paintExpandingBg(p, progress);
|
|
progress /= kFullDuration;
|
|
if (_footer) {
|
|
_footer->paintExpanding(
|
|
p,
|
|
rects.categories,
|
|
rects.radius,
|
|
RectPart::BottomRight);
|
|
}
|
|
_list->paintExpanding(
|
|
p,
|
|
rects.list.marginsRemoved(_st.margin),
|
|
rects.finalBottom,
|
|
rects.expanding,
|
|
progress,
|
|
RectPart::TopRight);
|
|
paintFadingExpandIcon(p, progress);
|
|
}
|
|
|
|
auto Selector::paintExpandingBg(QPainter &p, float64 progress)
|
|
-> ExpandingRects {
|
|
progress = (progress >= kExpandDuration)
|
|
? 1.
|
|
: (progress / kExpandDuration);
|
|
constexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;
|
|
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
|
|
const auto radiusStart = st::reactStripHeight / 2.;
|
|
const auto radiusEnd = st::emojiPanRadius;
|
|
const auto radius = _reactions.customAllowed
|
|
? (radiusStart + progress * (radiusEnd - radiusStart))
|
|
: radiusStart;
|
|
const auto extents = extentsForShadow();
|
|
const auto expanding = anim::easeOutCirc(1., progress);
|
|
const auto expandUp = anim::interpolate(0, _collapsedTopSkip, expanding);
|
|
const auto expandDown = anim::interpolate(
|
|
0,
|
|
(height() - _outer.y() - _outer.height()),
|
|
expanding);
|
|
const auto outer = _outer.marginsAdded({ 0, expandUp, 0, expandDown });
|
|
if (_useTransparency) {
|
|
const auto pattern = _cachedRound.validateFrame(frame, 1., radius);
|
|
const auto fill = _cachedRound.FillWithImage(p, outer, pattern);
|
|
if (!fill.isEmpty()) {
|
|
p.fillRect(fill, _st.bg);
|
|
}
|
|
} else {
|
|
const auto inner = outer.marginsRemoved(extentsForShadow());
|
|
p.fillRect(inner, _st.bg);
|
|
p.fillRect(
|
|
inner.x(),
|
|
inner.y() + inner.height(),
|
|
inner.width(),
|
|
st::lineWidth,
|
|
st::defaultPopupMenu.shadow.fallback);
|
|
}
|
|
const auto categories = anim::interpolate(
|
|
0,
|
|
extendTopForCategories(),
|
|
expanding);
|
|
const auto inner = outer.marginsRemoved(extents);
|
|
_shadowTop = inner.y() + categories;
|
|
_shadowSkip = (_useTransparency && categories < radius)
|
|
? int(base::SafeRound(
|
|
radius - sqrt(categories * (2 * radius - categories))))
|
|
: 0;
|
|
return {
|
|
.categories = QRect(inner.x(), inner.y(), inner.width(), categories),
|
|
.list = inner.marginsRemoved({ 0, categories, 0, 0 }),
|
|
.radius = radius,
|
|
.expanding = expanding,
|
|
.finalBottom = height() - extents.bottom(),
|
|
};
|
|
}
|
|
|
|
void Selector::paintFadingExpandIcon(QPainter &p, float64 progress) {
|
|
if (progress >= 1.) {
|
|
return;
|
|
}
|
|
p.setOpacity(1. - progress);
|
|
const auto sub = anim::interpolate(0, _size / 3, progress);
|
|
const auto expandIconPosition = _inner.topLeft()
|
|
+ QPoint(_inner.width() - _size - _skipx, _skipy);
|
|
const auto expandIconRect = QRect(
|
|
expandIconPosition,
|
|
QSize(_size, _size)
|
|
).marginsRemoved({ sub, sub, sub, sub });
|
|
p.drawImage(expandIconRect, _expandIconCache);
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
void Selector::paintExpanded(QPainter &p) {
|
|
if (!_expandFinished) {
|
|
finishExpand();
|
|
}
|
|
if (_useTransparency) {
|
|
p.drawImage(0, 0, _paintBuffer);
|
|
} else {
|
|
const auto inner = rect().marginsRemoved(extentsForShadow());
|
|
p.fillRect(inner, _st.bg);
|
|
p.fillRect(
|
|
inner.x(),
|
|
inner.y() + inner.height(),
|
|
inner.width(),
|
|
st::lineWidth,
|
|
st::defaultPopupMenu.shadow.fallback);
|
|
}
|
|
}
|
|
|
|
void Selector::finishExpand() {
|
|
Expects(!_expandFinished);
|
|
|
|
_expandFinished = true;
|
|
if (_useTransparency) {
|
|
auto q = QPainter(&_paintBuffer);
|
|
q.setCompositionMode(QPainter::CompositionMode_Source);
|
|
const auto pattern = _cachedRound.validateFrame(
|
|
kFramesCount - 1,
|
|
1.,
|
|
st::emojiPanRadius);
|
|
const auto fill = _cachedRound.FillWithImage(q, rect(), pattern);
|
|
if (!fill.isEmpty()) {
|
|
q.fillRect(fill, _st.bg);
|
|
}
|
|
}
|
|
if (_footer) {
|
|
_footer->show();
|
|
}
|
|
_scroll->show();
|
|
_list->afterShown();
|
|
_show->session().api().updateCustomEmoji();
|
|
}
|
|
|
|
void Selector::paintBubble(QPainter &p, int innerWidth) {
|
|
const auto &bubble = _st.icons.stripBubble;
|
|
const auto bubbleRight = std::min(
|
|
st::reactStripBubbleRight,
|
|
(innerWidth - bubble.width()) / 2);
|
|
bubble.paint(
|
|
p,
|
|
_inner.x() + innerWidth - bubbleRight - bubble.width(),
|
|
_inner.y() + _inner.height() - _collapsedTopSkip,
|
|
width());
|
|
}
|
|
|
|
void Selector::paintEvent(QPaintEvent *e) {
|
|
auto p = Painter(this);
|
|
if (_strip && _appearing && _useTransparency) {
|
|
paintAppearing(p);
|
|
} else if (_strip && !_expanded) {
|
|
paintCollapsed(p);
|
|
} else if (const auto progress = _expanding.value(kFullDuration)
|
|
; progress < kFullDuration) {
|
|
paintExpanding(p, progress);
|
|
} else {
|
|
paintExpanded(p);
|
|
}
|
|
}
|
|
|
|
void Selector::mouseMoveEvent(QMouseEvent *e) {
|
|
if (!_strip) {
|
|
return;
|
|
}
|
|
setSelected(lookupSelectedIndex(e->pos()));
|
|
}
|
|
|
|
int Selector::lookupSelectedIndex(QPoint position) const {
|
|
const auto p = position - _inner.topLeft() - QPoint(_skipx, _skipy);
|
|
const auto max = _strip->count();
|
|
const auto index = p.x() / _size;
|
|
if (p.x() >= 0 && p.y() >= 0 && p.y() < _inner.height() && index < max) {
|
|
return index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void Selector::setSelected(int index) {
|
|
Expects(_strip != nullptr);
|
|
|
|
if (index >= 0 && _expandScheduled) {
|
|
return;
|
|
}
|
|
_strip->setSelected(index);
|
|
const auto over = (index >= 0);
|
|
if (_over != over) {
|
|
_over = over;
|
|
setCursor(over ? style::cur_pointer : style::cur_default);
|
|
if (over) {
|
|
Ui::Integration::Instance().registerLeaveSubscription(this);
|
|
} else {
|
|
Ui::Integration::Instance().unregisterLeaveSubscription(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Selector::leaveEventHook(QEvent *e) {
|
|
if (!_strip) {
|
|
return;
|
|
}
|
|
setSelected(-1);
|
|
}
|
|
|
|
void Selector::mousePressEvent(QMouseEvent *e) {
|
|
if (!_strip) {
|
|
return;
|
|
}
|
|
_pressed = lookupSelectedIndex(e->pos());
|
|
}
|
|
|
|
void Selector::mouseReleaseEvent(QMouseEvent *e) {
|
|
if (!_strip || _pressed != lookupSelectedIndex(e->pos())) {
|
|
return;
|
|
}
|
|
_pressed = -1;
|
|
const auto selected = _strip->selected();
|
|
if (selected == Strip::AddedButton::Premium) {
|
|
_premiumPromoChosen.fire({});
|
|
} else if (selected == Strip::AddedButton::Expand) {
|
|
expand();
|
|
} else if (const auto id = std::get_if<Data::ReactionId>(&selected)) {
|
|
if (!id->empty()) {
|
|
_chosen.fire(lookupChosen(*id));
|
|
}
|
|
}
|
|
}
|
|
|
|
ChosenReaction Selector::lookupChosen(const Data::ReactionId &id) const {
|
|
Expects(_strip != nullptr);
|
|
|
|
auto result = ChosenReaction{
|
|
.id = id,
|
|
};
|
|
const auto index = _strip->fillChosenIconGetIndex(result);
|
|
if (result.icon.isNull()) {
|
|
return result;
|
|
}
|
|
const auto rect = QRect(_skipx + index * _size, _skipy, _size, _size);
|
|
const auto imageSize = _strip->computeOverSize();
|
|
result.globalGeometry = mapToGlobal(QRect(
|
|
_inner.x() + rect.x() + (rect.width() - imageSize) / 2,
|
|
_inner.y() + rect.y() + (rect.height() - imageSize) / 2,
|
|
imageSize,
|
|
imageSize));
|
|
return result;
|
|
}
|
|
|
|
void Selector::preloadAllRecentsAnimations() {
|
|
const auto preload = [&](DocumentData *document) {
|
|
const auto view = document
|
|
? document->activeMediaView()
|
|
: nullptr;
|
|
if (view) {
|
|
view->checkStickerLarge();
|
|
}
|
|
};
|
|
for (const auto &reaction : _reactions.recent) {
|
|
if (!reaction.id.custom()) {
|
|
preload(reaction.centerIcon);
|
|
}
|
|
preload(reaction.aroundAnimation);
|
|
}
|
|
}
|
|
|
|
void Selector::expand() {
|
|
if (_expandScheduled) {
|
|
return;
|
|
}
|
|
_expandScheduled = true;
|
|
_willExpand.fire({});
|
|
preloadAllRecentsAnimations();
|
|
const auto parent = parentWidget()->geometry();
|
|
const auto extents = extentsForShadow();
|
|
const auto heightLimit = _reactions.customAllowed
|
|
? st::emojiPanMaxHeight
|
|
: minimalHeight();
|
|
const auto willBeHeight = std::min(
|
|
parent.height() - y(),
|
|
extents.top() + heightLimit + extents.bottom());
|
|
const auto additionalBottom = willBeHeight - height();
|
|
const auto additional = _specialExpandTopSkip + additionalBottom;
|
|
if (additionalBottom < 0 || additional <= 0) {
|
|
return;
|
|
} else if (additionalBottom > 0) {
|
|
resize(width(), height() + additionalBottom);
|
|
raise();
|
|
}
|
|
|
|
createList();
|
|
cacheExpandIcon();
|
|
|
|
[[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll);
|
|
_list->prepareExpanding();
|
|
setSelected(-1);
|
|
|
|
base::call_delayed(kExpandDelay, this, [this] {
|
|
const auto full = kExpandDuration + kScaleDuration;
|
|
_expanded = true;
|
|
_paintBuffer = _cachedRound.PrepareImage(size());
|
|
_expanding.start([=] { update(); }, 0., full, full);
|
|
});
|
|
}
|
|
|
|
void Selector::cacheExpandIcon() {
|
|
if (!_strip) {
|
|
return;
|
|
}
|
|
_expandIconCache = _cachedRound.PrepareImage({ _size, _size });
|
|
_expandIconCache.fill(Qt::transparent);
|
|
auto q = QPainter(&_expandIconCache);
|
|
_strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.);
|
|
}
|
|
|
|
void Selector::createList() {
|
|
using namespace ChatHelpers;
|
|
auto recent = _recent;
|
|
auto defaultReactionIds = base::flat_map<DocumentId, QString>();
|
|
if (_strip) {
|
|
recent.reserve(recentCount());
|
|
auto index = 0;
|
|
const auto inStrip = _strip->count();
|
|
for (const auto &reaction : _reactions.recent) {
|
|
if (const auto id = reaction.id.custom()) {
|
|
recent.push_back(id);
|
|
} else {
|
|
recent.push_back(reaction.selectAnimation->id);
|
|
defaultReactionIds.emplace(recent.back(), reaction.id.emoji());
|
|
}
|
|
if (index + 1 < inStrip) {
|
|
_defaultReactionInStripMap.emplace(recent.back(), index++);
|
|
}
|
|
};
|
|
}
|
|
const auto manager = &_show->session().data().customEmojiManager();
|
|
_stripPaintOneShift = [&] {
|
|
// See EmojiListWidget custom emoji position resolving.
|
|
const auto area = st::emojiPanArea;
|
|
const auto areaPosition = QPoint(
|
|
(_size - area.width()) / 2,
|
|
(_size - area.height()) / 2);
|
|
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
|
|
const auto innerPosition = QPoint(
|
|
(area.width() - esize) / 2,
|
|
(area.height() - esize) / 2);
|
|
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
|
const auto customSkip = (esize - customSize) / 2;
|
|
const auto customPosition = QPoint(customSkip, customSkip);
|
|
return areaPosition + innerPosition + customPosition;
|
|
}();
|
|
_defaultReactionShift = QPoint(
|
|
(_size - st::reactStripImage) / 2,
|
|
(_size - st::reactStripImage) / 2
|
|
) - _stripPaintOneShift;
|
|
auto factory = [=](DocumentId id, Fn<void()> repaint)
|
|
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
|
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
|
|
const auto sizeOverride = st::reactStripImage;
|
|
const auto isDefaultReaction = defaultReactionIds.contains(id);
|
|
auto result = isDefaultReaction
|
|
? std::make_unique<Ui::Text::ShiftedEmoji>(
|
|
manager->create(id, std::move(repaint), tag, sizeOverride),
|
|
_defaultReactionShift)
|
|
: manager->create(id, std::move(repaint), tag);
|
|
const auto i = _defaultReactionInStripMap.find(id);
|
|
if (i != end(_defaultReactionInStripMap)) {
|
|
Assert(_strip != nullptr);
|
|
return std::make_unique<StripEmoji>(
|
|
std::move(result),
|
|
_strip.get(),
|
|
-_stripPaintOneShift,
|
|
i->second);
|
|
}
|
|
return result;
|
|
};
|
|
_scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::reactPanelScroll);
|
|
_scroll->hide();
|
|
|
|
const auto st = lifetime().make_state<style::EmojiPan>(_st);
|
|
st->padding.setTop(_skipy);
|
|
if (!_reactions.customAllowed) {
|
|
st->bg = st::transparent;
|
|
}
|
|
_list = _scroll->setOwnedWidget(
|
|
object_ptr<EmojiListWidget>(_scroll, EmojiListDescriptor{
|
|
.show = _show,
|
|
.mode = _listMode,
|
|
.paused = [] { return false; },
|
|
.customRecentList = std::move(recent),
|
|
.customRecentFactory = std::move(factory),
|
|
.st = st,
|
|
})
|
|
).data();
|
|
|
|
_list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime());
|
|
|
|
_list->customChosen(
|
|
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
|
const auto id = DocumentId{ data.document->id };
|
|
const auto i = defaultReactionIds.find(id);
|
|
const auto reactionId = (i != end(defaultReactionIds))
|
|
? Data::ReactionId{ i->second }
|
|
: Data::ReactionId{ id };
|
|
_chosen.fire({
|
|
.id = reactionId,
|
|
.icon = data.messageSendingFrom.frame,
|
|
.globalGeometry = data.messageSendingFrom.globalStartGeometry,
|
|
});
|
|
}, _list->lifetime());
|
|
|
|
_list->jumpedToPremium(
|
|
) | rpl::start_with_next(_jumpedToPremium, _list->lifetime());
|
|
|
|
const auto inner = rect().marginsRemoved(extentsForShadow());
|
|
const auto footer = _reactions.customAllowed
|
|
? _list->createFooter().data()
|
|
: nullptr;
|
|
if ((_footer = static_cast<StickersListFooter*>(footer))) {
|
|
_footer->setParent(this);
|
|
_footer->hide();
|
|
_footer->setGeometry(
|
|
inner.x(),
|
|
inner.y(),
|
|
inner.width(),
|
|
_footer->height());
|
|
_shadowTop = _outer.y();
|
|
_shadowSkip = _useTransparency ? (st::reactStripHeight / 2) : 0;
|
|
_shadow = Ui::CreateChild<Ui::PlainShadow>(this);
|
|
rpl::combine(
|
|
_shadowTop.value(),
|
|
_shadowSkip.value()
|
|
) | rpl::start_with_next([=](int top, int skip) {
|
|
_shadow->setGeometry(
|
|
inner.x() + skip,
|
|
top,
|
|
inner.width() - 2 * skip,
|
|
st::lineWidth);
|
|
}, _shadow->lifetime());
|
|
_shadow->show();
|
|
}
|
|
const auto geometry = inner.marginsRemoved(_st.margin);
|
|
_list->move(0, 0);
|
|
_list->resizeToWidth(geometry.width());
|
|
_list->refreshEmoji();
|
|
_list->show();
|
|
|
|
const auto updateVisibleTopBottom = [=] {
|
|
const auto scrollTop = _scroll->scrollTop();
|
|
const auto scrollBottom = scrollTop + _scroll->height();
|
|
_list->setVisibleTopBottom(scrollTop, scrollBottom);
|
|
};
|
|
_scroll->scrollTopChanges(
|
|
) | rpl::start_with_next(updateVisibleTopBottom, _list->lifetime());
|
|
|
|
_list->scrollToRequests(
|
|
) | rpl::start_with_next([=](int y) {
|
|
_scroll->scrollToY(y);
|
|
_shadow->update();
|
|
}, _list->lifetime());
|
|
|
|
_scroll->setGeometry(inner.marginsRemoved({
|
|
_st.margin.left(),
|
|
_footer ? _footer->height() : 0,
|
|
0,
|
|
0,
|
|
}));
|
|
_list->setMinimalHeight(geometry.width(), _scroll->height());
|
|
|
|
updateVisibleTopBottom();
|
|
}
|
|
|
|
bool AdjustMenuGeometryForSelector(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
QPoint desiredPosition,
|
|
not_null<Selector*> selector) {
|
|
const auto useTransparency = selector->useTransparency();
|
|
const auto extend = useTransparency
|
|
? st::reactStripExtend
|
|
: QMargins(0, st::lineWidth + st::reactStripHeight, 0, 0);
|
|
const auto added = extend.left() + extend.right();
|
|
const auto desiredWidth = menu->menu()->width() + added;
|
|
const auto maxWidth = menu->st().menu.widthMax + added;
|
|
const auto width = selector->countWidth(desiredWidth, maxWidth);
|
|
const auto extents = selector->extentsForShadow();
|
|
const auto categoriesTop = selector->useTransparency()
|
|
? selector->extendTopForCategories()
|
|
: 0;
|
|
menu->setForceWidth(width - added);
|
|
const auto height = menu->height();
|
|
const auto fullTop = extents.top() + categoriesTop + extend.top();
|
|
const auto minimalHeight = extents.top()
|
|
+ selector->minimalHeight()
|
|
+ extents.bottom();
|
|
const auto willBeHeightWithoutBottomPadding = fullTop
|
|
+ height
|
|
- menu->st().shadow.extend.top();
|
|
const auto additionalPaddingBottom
|
|
= (willBeHeightWithoutBottomPadding >= minimalHeight
|
|
? 0
|
|
: useTransparency
|
|
? (minimalHeight - willBeHeightWithoutBottomPadding)
|
|
: 0);
|
|
menu->setAdditionalMenuPadding(QMargins(
|
|
extents.left() + extend.left(),
|
|
fullTop,
|
|
extents.right() + extend.right(),
|
|
additionalPaddingBottom
|
|
), QMargins(
|
|
extents.left(),
|
|
extents.top(),
|
|
extents.right(),
|
|
std::min(additionalPaddingBottom, extents.bottom())
|
|
));
|
|
if (!menu->prepareGeometryFor(desiredPosition)) {
|
|
return false;
|
|
}
|
|
const auto origin = menu->preparedOrigin();
|
|
if (!additionalPaddingBottom
|
|
|| origin == Ui::PanelAnimation::Origin::TopLeft
|
|
|| origin == Ui::PanelAnimation::Origin::TopRight) {
|
|
return true;
|
|
}
|
|
menu->setAdditionalMenuPadding(QMargins(
|
|
extents.left() + extend.left(),
|
|
fullTop + additionalPaddingBottom,
|
|
extents.right() + extend.right(),
|
|
0
|
|
), QMargins(
|
|
extents.left(),
|
|
extents.top(),
|
|
extents.right(),
|
|
0
|
|
));
|
|
selector->setSpecialExpandTopSkip(additionalPaddingBottom);
|
|
return menu->prepareGeometryFor(desiredPosition);
|
|
}
|
|
|
|
AttachSelectorResult MakeJustSelectorMenu(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
not_null<Window::SessionController*> controller,
|
|
QPoint desiredPosition,
|
|
ChatHelpers::EmojiListMode mode,
|
|
std::vector<DocumentId> recent,
|
|
Fn<void(ChosenReaction)> chosen) {
|
|
const auto selector = Ui::CreateChild<Selector>(
|
|
menu.get(),
|
|
st::reactPanelEmojiPan,
|
|
controller->uiShow(),
|
|
mode,
|
|
std::move(recent),
|
|
[=](bool fast) { menu->hideMenu(fast); },
|
|
false); // child
|
|
if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
|
|
return AttachSelectorResult::Failed;
|
|
}
|
|
if (mode != ChatHelpers::EmojiListMode::RecentReactions) {
|
|
Ui::Platform::FixPopupMenuNativeEmojiPopup(menu);
|
|
}
|
|
const auto selectorInnerTop = menu->preparedPadding().top()
|
|
- st::reactStripExtend.top();
|
|
menu->animatePhaseValue(
|
|
) | rpl::start_with_next([=](Ui::PopupMenu::AnimatePhase phase) {
|
|
if (phase == Ui::PopupMenu::AnimatePhase::StartHide) {
|
|
selector->beforeDestroy();
|
|
}
|
|
}, selector->lifetime());
|
|
selector->initGeometry(selectorInnerTop);
|
|
selector->show();
|
|
|
|
selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) {
|
|
menu->hideMenu();
|
|
chosen(std::move(reaction));
|
|
}, selector->lifetime());
|
|
|
|
const auto correctTop = selector->y();
|
|
menu->showStateValue(
|
|
) | rpl::start_with_next([=](Ui::PopupMenu::ShowState state) {
|
|
const auto origin = menu->preparedOrigin();
|
|
using Origin = Ui::PanelAnimation::Origin;
|
|
if (origin == Origin::BottomLeft || origin == Origin::BottomRight) {
|
|
const auto add = state.appearing
|
|
? (menu->rect().marginsRemoved(
|
|
menu->preparedPadding()
|
|
).height() - state.appearingHeight)
|
|
: 0;
|
|
selector->move(selector->x(), correctTop + add);
|
|
}
|
|
selector->updateShowState(
|
|
state.widthProgress * state.heightProgress,
|
|
state.opacity,
|
|
state.appearing,
|
|
state.toggling);
|
|
}, selector->lifetime());
|
|
|
|
const auto weak = base::make_weak(controller);
|
|
controller->enableGifPauseReason(
|
|
Window::GifPauseReason::MediaPreview);
|
|
QObject::connect(menu.get(), &QObject::destroyed, [weak] {
|
|
if (const auto strong = weak.get()) {
|
|
strong->disableGifPauseReason(
|
|
Window::GifPauseReason::MediaPreview);
|
|
}
|
|
});
|
|
|
|
return AttachSelectorResult::Attached;
|
|
}
|
|
|
|
AttachSelectorResult AttachSelectorToMenu(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
not_null<Window::SessionController*> controller,
|
|
QPoint desiredPosition,
|
|
not_null<HistoryItem*> item,
|
|
Fn<void(ChosenReaction)> chosen,
|
|
Fn<void(FullMsgId)> showPremiumPromo,
|
|
IconFactory iconFactory) {
|
|
const auto result = AttachSelectorToMenu(
|
|
menu,
|
|
desiredPosition,
|
|
st::reactPanelEmojiPan,
|
|
controller->uiShow(),
|
|
Data::LookupPossibleReactions(item),
|
|
std::move(iconFactory));
|
|
if (!result) {
|
|
return result.error();
|
|
}
|
|
const auto selector = *result;
|
|
const auto itemId = item->fullId();
|
|
|
|
selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) {
|
|
menu->hideMenu();
|
|
reaction.context = itemId;
|
|
chosen(std::move(reaction));
|
|
}, selector->lifetime());
|
|
|
|
selector->premiumPromoChosen() | rpl::start_with_next([=] {
|
|
menu->hideMenu();
|
|
showPremiumPromo(itemId);
|
|
}, selector->lifetime());
|
|
|
|
const auto weak = base::make_weak(controller);
|
|
controller->enableGifPauseReason(
|
|
Window::GifPauseReason::MediaPreview);
|
|
QObject::connect(menu.get(), &QObject::destroyed, [weak] {
|
|
if (const auto strong = weak.get()) {
|
|
strong->disableGifPauseReason(
|
|
Window::GifPauseReason::MediaPreview);
|
|
}
|
|
});
|
|
|
|
return AttachSelectorResult::Attached;
|
|
}
|
|
|
|
auto AttachSelectorToMenu(
|
|
not_null<Ui::PopupMenu*> menu,
|
|
QPoint desiredPosition,
|
|
const style::EmojiPan &st,
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
const Data::PossibleItemReactionsRef &reactions,
|
|
IconFactory iconFactory)
|
|
-> base::expected<not_null<Selector*>, AttachSelectorResult> {
|
|
if (reactions.recent.empty() && !reactions.morePremiumAvailable) {
|
|
return base::make_unexpected(AttachSelectorResult::Skipped);
|
|
}
|
|
const auto withSearch = reactions.customAllowed;
|
|
const auto selector = Ui::CreateChild<Selector>(
|
|
menu.get(),
|
|
st,
|
|
std::move(show),
|
|
std::move(reactions),
|
|
std::move(iconFactory),
|
|
[=](bool fast) { menu->hideMenu(fast); },
|
|
false); // child
|
|
if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
|
|
return base::make_unexpected(AttachSelectorResult::Failed);
|
|
}
|
|
if (withSearch) {
|
|
Ui::Platform::FixPopupMenuNativeEmojiPopup(menu);
|
|
}
|
|
const auto selectorInnerTop = selector->useTransparency()
|
|
? (menu->preparedPadding().top() - st::reactStripExtend.top())
|
|
: st::lineWidth;
|
|
menu->animatePhaseValue(
|
|
) | rpl::start_with_next([=](Ui::PopupMenu::AnimatePhase phase) {
|
|
if (phase == Ui::PopupMenu::AnimatePhase::StartHide) {
|
|
selector->beforeDestroy();
|
|
}
|
|
}, selector->lifetime());
|
|
selector->initGeometry(selectorInnerTop);
|
|
selector->show();
|
|
|
|
const auto correctTop = selector->y();
|
|
menu->showStateValue(
|
|
) | rpl::start_with_next([=](Ui::PopupMenu::ShowState state) {
|
|
const auto origin = menu->preparedOrigin();
|
|
using Origin = Ui::PanelAnimation::Origin;
|
|
if (origin == Origin::BottomLeft || origin == Origin::BottomRight) {
|
|
const auto add = state.appearing
|
|
? (menu->rect().marginsRemoved(
|
|
menu->preparedPadding()
|
|
).height() - state.appearingHeight)
|
|
: 0;
|
|
selector->move(selector->x(), correctTop + add);
|
|
}
|
|
selector->updateShowState(
|
|
state.widthProgress * state.heightProgress,
|
|
state.opacity,
|
|
state.appearing,
|
|
state.toggling);
|
|
}, selector->lifetime());
|
|
|
|
return selector;
|
|
}
|
|
|
|
} // namespace HistoryView::Reactions
|