/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "chat_helpers/stickers_list_widget.h" #include "styles/style_chat_helpers.h" #include "ui/widgets/buttons.h" #include "ui/effects/ripple_animation.h" #include "boxes/stickers_box.h" #include "inline_bots/inline_bot_result.h" #include "chat_helpers/stickers.h" #include "storage/localstorage.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "dialogs/dialogs_layout.h" #include "boxes/sticker_set_box.h" #include "boxes/stickers_box.h" #include "boxes/confirm_box.h" namespace ChatHelpers { namespace { constexpr auto kStickersPanelPerRow = Stickers::kPanelPerRow; constexpr auto kInlineItemsMaxPerRow = 5; } // namespace struct StickerIcon { StickerIcon(uint64 setId) : setId(setId) { } StickerIcon(uint64 setId, DocumentData *sticker, int32 pixw, int32 pixh) : setId(setId), sticker(sticker), pixw(pixw), pixh(pixh) { } uint64 setId = 0; DocumentData *sticker = nullptr; int pixw = 0; int pixh = 0; }; class StickersListWidget::Footer : public TabbedSelector::InnerFooter, private base::Subscriber { public: Footer(gsl::not_null parent); void preloadImages(); void validateSelectedIcon(uint64 setId, ValidateIconAnimations animations); void refreshIcons(ValidateIconAnimations animations); bool hasOnlyFeaturedSets() const; void leaveToChildEvent(QEvent *e, QWidget *child) override; protected: void paintEvent(QPaintEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; bool event(QEvent *e) override; void processHideFinished() override; private: template void enumerateVisibleIcons(Callback callback); void step_icons(TimeMs ms, bool timer); void updateSelected(); void paintStickerSettingsIcon(Painter &p) const; void paintFeaturedStickerSetsBadge(Painter &p, int iconLeft) const; gsl::not_null _pan; static constexpr auto kVisibleIconsCount = 8; QList _icons; int _iconOver = -1; int _iconSel = 0; int _iconDown = -1; bool _iconsDragging = false; BasicAnimation _a_icons; QPoint _iconsMousePos, _iconsMouseDown; int _iconsLeft = 0; int _iconsTop = 0; int _iconsStartX = 0; int _iconsMax = 0; anim::value _iconsX; anim::value _iconSelX; TimeMs _iconsStartAnim = 0; bool _horizontal = false; }; StickersListWidget::Footer::Footer(gsl::not_null parent) : InnerFooter(parent) , _pan(parent) , _a_icons(animation(this, &Footer::step_icons)) { setMouseTracking(true); _iconsLeft = (st::emojiPanWidth - kVisibleIconsCount * st::emojiCategory.width) / 2; subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { update(); }); } template void StickersListWidget::Footer::enumerateVisibleIcons(Callback callback) { auto iconsX = qRound(_iconsX.current()); auto index = iconsX / st::emojiCategory.width; auto x = _iconsLeft - (iconsX % st::emojiCategory.width); for (auto index = qMin(_icons.size(), iconsX / st::emojiCategory.width), last = qMin(_icons.size(), index + kVisibleIconsCount); index != last; ++index) { callback(_icons[index], x); x += st::emojiCategory.width; } } void StickersListWidget::Footer::preloadImages() { enumerateVisibleIcons([this](const StickerIcon &icon, int x) { if (auto sticker = icon.sticker) { sticker->thumb->load(); } }); } void StickersListWidget::Footer::validateSelectedIcon(uint64 setId, ValidateIconAnimations animations) { auto newSel = 0; for (auto i = 0, l = _icons.size(); i != l; ++i) { if (_icons[i].setId == setId) { newSel = i; break; } } if (newSel != _iconSel) { _iconSel = newSel; auto iconSelXFinal = _iconSel * st::emojiCategory.width; if (animations == ValidateIconAnimations::Full) { _iconSelX.start(iconSelXFinal); } else { _iconSelX = anim::value(iconSelXFinal, iconSelXFinal); } auto iconsXFinal = snap((2 * _iconSel + 1 - kVisibleIconsCount) * st::emojiCategory.width / 2, 0, _iconsMax); if (animations == ValidateIconAnimations::None) { _iconsX = anim::value(iconsXFinal, iconsXFinal); _a_icons.stop(); } else { _iconsX.start(iconsXFinal); _iconsStartAnim = getms(); _a_icons.start(); } updateSelected(); update(); } } void StickersListWidget::Footer::processHideFinished() { _iconOver = _iconDown = -1; _iconsStartAnim = 0; _a_icons.stop(); _iconsX.finish(); _iconSelX.finish(); _horizontal = false; } void StickersListWidget::Footer::leaveToChildEvent(QEvent *e, QWidget *child) { _iconsMousePos = QCursor::pos(); updateSelected(); } void StickersListWidget::Footer::paintEvent(QPaintEvent *e) { Painter p(this); if (_icons.isEmpty()) { return; } if (!hasOnlyFeaturedSets()) { paintStickerSettingsIcon(p); } auto selxrel = _iconsLeft + qRound(_iconSelX.current()); auto selx = selxrel - qRound(_iconsX.current()); auto clip = QRect(_iconsLeft, _iconsTop, _iconsLeft + (kVisibleIconsCount - 1) * st::emojiCategory.width - _iconsLeft, st::emojiCategory.height); if (rtl()) clip.moveLeft(width() - _iconsLeft - clip.width()); p.setClipRect(clip); enumerateVisibleIcons([this, &p](const StickerIcon &icon, int x) { if (icon.sticker) { icon.sticker->thumb->load(); auto pix = icon.sticker->thumb->pix(icon.pixw, icon.pixh); p.drawPixmapLeft(x + (st::emojiCategory.width - icon.pixw) / 2, _iconsTop + (st::emojiCategory.height - icon.pixh) / 2, width(), pix); } else { auto getSpecialSetIcon = [](uint64 setId) { if (setId == Stickers::FeaturedSetId) { return &st::stickersTrending; } else if (setId == Stickers::FavedSetId) { return &st::stickersFaved; } return &st::emojiRecent; }; getSpecialSetIcon(icon.setId)->paint(p, x + st::emojiCategory.iconPosition.x(), _iconsTop + st::emojiCategory.iconPosition.y(), width()); if (icon.setId == Stickers::FeaturedSetId) { paintFeaturedStickerSetsBadge(p, x); } } }); if (rtl()) selx = width() - selx - st::emojiCategory.width; p.fillRect(selx, _iconsTop + st::emojiCategory.height - st::stickerIconPadding, st::emojiCategory.width, st::stickerIconSel, st::stickerIconSelColor); auto o_left = snap(_iconsX.current() / st::stickerIconLeft.width(), 0., 1.); if (o_left > 0) { p.setOpacity(o_left); st::stickerIconLeft.fill(p, rtlrect(_iconsLeft, _iconsTop, st::stickerIconLeft.width(), st::emojiCategory.height, width())); p.setOpacity(1.); } auto o_right = snap((_iconsMax - _iconsX.current()) / st::stickerIconRight.width(), 0., 1.); if (o_right > 0) { p.setOpacity(o_right); st::stickerIconRight.fill(p, rtlrect(_iconsLeft + (kVisibleIconsCount - 1) * st::emojiCategory.width - st::stickerIconRight.width(), _iconsTop, st::stickerIconRight.width(), st::emojiCategory.height, width())); p.setOpacity(1.); } } void StickersListWidget::Footer::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; } _iconsMousePos = e ? e->globalPos() : QCursor::pos(); updateSelected(); if (_iconOver == _icons.size()) { Ui::show(Box(hasOnlyFeaturedSets() ? StickersBox::Section::Featured : StickersBox::Section::Installed)); } else { _iconDown = _iconOver; _iconsMouseDown = _iconsMousePos; _iconsStartX = qRound(_iconsX.current()); } } void StickersListWidget::Footer::mouseMoveEvent(QMouseEvent *e) { _iconsMousePos = e ? e->globalPos() : QCursor::pos(); updateSelected(); if (!_iconsDragging && !_icons.isEmpty() && _iconDown >= 0) { if ((_iconsMousePos - _iconsMouseDown).manhattanLength() >= QApplication::startDragDistance()) { _iconsDragging = true; } } if (_iconsDragging) { auto newX = snap(_iconsStartX + (rtl() ? -1 : 1) * (_iconsMouseDown.x() - _iconsMousePos.x()), 0, _iconsMax); if (newX != qRound(_iconsX.current())) { _iconsX = anim::value(newX, newX); _iconsStartAnim = 0; _a_icons.stop(); update(); } } } void StickersListWidget::Footer::mouseReleaseEvent(QMouseEvent *e) { if (_icons.isEmpty()) { return; } auto wasDown = _iconDown; _iconDown = -1; _iconsMousePos = e ? e->globalPos() : QCursor::pos(); if (_iconsDragging) { auto newX = snap(_iconsStartX + _iconsMouseDown.x() - _iconsMousePos.x(), 0, _iconsMax); if (newX != qRound(_iconsX.current())) { _iconsX = anim::value(newX, newX); _iconsStartAnim = 0; _a_icons.stop(); update(); } _iconsDragging = false; updateSelected(); } else { updateSelected(); if (wasDown == _iconOver && _iconOver >= 0 && _iconOver < _icons.size()) { _iconSelX = anim::value(_iconOver * st::emojiCategory.width, _iconOver * st::emojiCategory.width); _pan->showStickerSet(_icons[_iconOver].setId); } } } bool StickersListWidget::Footer::event(QEvent *e) { if (e->type() == QEvent::TouchBegin) { } else if (e->type() == QEvent::Wheel) { if (!_icons.isEmpty() && _iconOver >= 0 && _iconOver < _icons.size() && _iconDown < 0) { auto ev = static_cast(e); auto horizontal = (ev->angleDelta().x() != 0 || ev->orientation() == Qt::Horizontal); auto vertical = (ev->angleDelta().y() != 0 || ev->orientation() == Qt::Vertical); if (horizontal) _horizontal = true; auto newX = qRound(_iconsX.current()); if (/*_horizontal && */horizontal) { newX = snap(newX - (rtl() ? -1 : 1) * (ev->pixelDelta().x() ? ev->pixelDelta().x() : ev->angleDelta().x()), 0, _iconsMax); } else if (/*!_horizontal && */vertical) { newX = snap(newX - (ev->pixelDelta().y() ? ev->pixelDelta().y() : ev->angleDelta().y()), 0, _iconsMax); } if (newX != qRound(_iconsX.current())) { _iconsX = anim::value(newX, newX); _iconsStartAnim = 0; _a_icons.stop(); updateSelected(); update(); } } } return TWidget::event(e); } void StickersListWidget::Footer::updateSelected() { if (_iconDown >= 0) { return; } auto p = mapFromGlobal(_iconsMousePos); int32 x = p.x(), y = p.y(), newOver = -1; if (rtl()) x = width() - x; x -= _iconsLeft; if (x >= st::emojiCategory.width * (kVisibleIconsCount - 1) && x < st::emojiCategory.width * kVisibleIconsCount && y >= _iconsTop && y < _iconsTop + st::emojiCategory.height) { if (!_icons.isEmpty() && !hasOnlyFeaturedSets()) { newOver = _icons.size(); } } else if (!_icons.isEmpty()) { if (y >= _iconsTop && y < _iconsTop + st::emojiCategory.height && x >= 0 && x < (kVisibleIconsCount - 1) * st::emojiCategory.width && x < _icons.size() * st::emojiCategory.width) { x += qRound(_iconsX.current()); newOver = qFloor(x / st::emojiCategory.width); } } if (newOver != _iconOver) { if (newOver < 0) { setCursor(style::cur_default); } else if (_iconOver < 0) { setCursor(style::cur_pointer); } _iconOver = newOver; } } void StickersListWidget::Footer::refreshIcons(ValidateIconAnimations animations) { _iconOver = -1; _pan->fillIcons(_icons); _iconsX.finish(); _iconSelX.finish(); _iconsStartAnim = 0; _a_icons.stop(); if (_icons.isEmpty()) { _iconsMax = 0; } else { _iconsMax = qMax(int((_icons.size() + 1 - kVisibleIconsCount) * st::emojiCategory.width), 0); } if (_iconsX.current() > _iconsMax) { _iconsX = anim::value(_iconsMax, _iconsMax); } updateSelected(); _pan->validateSelectedIcon(animations); update(); } bool StickersListWidget::Footer::hasOnlyFeaturedSets() const { return (_icons.size() == 1) && (_icons[0].setId == Stickers::FeaturedSetId); } void StickersListWidget::Footer::paintStickerSettingsIcon(Painter &p) const { int settingsLeft = _iconsLeft + (kVisibleIconsCount - 1) * st::emojiCategory.width; st::stickersSettings.paint(p, settingsLeft + st::emojiCategory.iconPosition.x(), _iconsTop + st::emojiCategory.iconPosition.y(), width()); } void StickersListWidget::Footer::paintFeaturedStickerSetsBadge(Painter &p, int iconLeft) const { if (auto unread = Global::FeaturedStickerSetsUnreadCount()) { Dialogs::Layout::UnreadBadgeStyle unreadSt; unreadSt.sizeId = Dialogs::Layout::UnreadBadgeInStickersPanel; unreadSt.size = st::stickersSettingsUnreadSize; int unreadRight = iconLeft + st::emojiCategory.width - st::stickersSettingsUnreadPosition.x(); if (rtl()) unreadRight = width() - unreadRight; int unreadTop = _iconsTop + st::stickersSettingsUnreadPosition.y(); Dialogs::Layout::paintUnreadCount(p, QString::number(unread), unreadRight, unreadTop, unreadSt); } } void StickersListWidget::validateSelectedIcon(ValidateIconAnimations animations) { if (_footer) { _footer->validateSelectedIcon(currentSet(getVisibleTop()), animations); } } void StickersListWidget::Footer::step_icons(TimeMs ms, bool timer) { if (_iconsStartAnim) { auto dt = (ms - _iconsStartAnim) / float64(st::stickerIconMove); if (dt >= 1) { _iconsStartAnim = 0; _iconsX.finish(); _iconSelX.finish(); } else { _iconsX.update(dt, anim::linear); _iconSelX.update(dt, anim::linear); } } if (timer) update(); if (!_iconsStartAnim) { _a_icons.stop(); } } StickersListWidget::StickersListWidget(QWidget *parent, gsl::not_null controller) : Inner(parent, controller) , _section(Section::Stickers) , _addText(lang(lng_stickers_featured_add).toUpper()) , _addWidth(st::stickersTrendingAdd.font->width(_addText)) , _settings(this, lang(lng_stickers_you_have)) { resize(st::emojiPanWidth - st::emojiScroll.width - st::buttonRadius, countHeight()); setMouseTracking(true); setAttribute(Qt::WA_OpaquePaintEvent); connect(_settings, SIGNAL(clicked()), this, SLOT(onSettings())); _previewTimer.setSingleShot(true); connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreview())); subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { update(); readVisibleSets(); }); } object_ptr StickersListWidget::createFooter() { Expects(_footer == nullptr); auto result = object_ptr