/* 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 "stickers/emoji_list_widget.h" #include "styles/style_stickers.h" #include "ui/widgets/shadow.h" #include "lang.h" namespace ChatHelpers { namespace { constexpr auto kEmojiPanelPerRow = Ui::Emoji::kPanelPerRow; constexpr auto kEmojiPanelRowsPerPage = Ui::Emoji::kPanelRowsPerPage; constexpr auto kSaveRecentEmojiTimeout = 3000; } // namespace EmojiColorPicker::EmojiColorPicker(QWidget *parent) : TWidget(parent) { setMouseTracking(true); auto w = st::emojiPanMargins.left() + st::emojiPanSize.width() + st::emojiColorsSep + st::emojiPanMargins.right(); auto h = st::emojiPanMargins.top() + 2 * st::emojiColorsPadding + st::emojiPanSize.height() + st::emojiPanMargins.bottom(); resize(w, h); _hideTimer.setSingleShot(true); connect(&_hideTimer, SIGNAL(timeout()), this, SLOT(hideAnimated())); } void EmojiColorPicker::showEmoji(EmojiPtr emoji) { if (!emoji || !emoji->hasVariants()) { return; } _ignoreShow = false; _variants.resize(emoji->variantsCount() + 1); for (auto i = 0, size = _variants.size(); i != size; ++i) { _variants[i] = emoji->variant(i); } auto w = st::emojiPanMargins.left() + st::emojiPanSize.width() * _variants.size() + (_variants.size() - 2) * st::emojiColorsPadding + st::emojiColorsSep + st::emojiPanMargins.right(); auto h = st::emojiPanMargins.top() + 2 * st::emojiColorsPadding + st::emojiPanSize.height() + st::emojiPanMargins.bottom(); resize(w, h); if (!_cache.isNull()) _cache = QPixmap(); showAnimated(); } void EmojiColorPicker::paintEvent(QPaintEvent *e) { Painter p(this); auto opacity = _a_opacity.current(getms(), _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::defaultRoundShadow); App::roundRect(p, inner, st::boxBg, BoxCorners); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + st::emojiPanSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; p.fillRect(x, st::emojiPanMargins.top() + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2, st::emojiColorsSepColor); if (_variants.isEmpty()) return; for (auto i = 0, count = _variants.size(); i != count; ++i) { drawVariant(p, i); } } void EmojiColorPicker::enterEventHook(QEvent *e) { _hideTimer.stop(); if (_hiding) showAnimated(); TWidget::enterEventHook(e); } void EmojiColorPicker::leaveEventHook(QEvent *e) { TWidget::leaveEventHook(e); } 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)) { emit emojiSelected(_variants[_selected]); } _ignoreShow = true; hideAnimated(); } 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 (_hiding) { hide(); emit hidden(); } else { _lastMousePos = QCursor::pos(); updateSelected(); } } } void EmojiColorPicker::hideFast() { clearSelection(); _a_opacity.finish(); _cache = QPixmap(); hide(); emit hidden(); } void EmojiColorPicker::hideAnimated() { if (_cache.isNull()) { _cache = myGrab(this); clearSelection(); } _hiding = true; _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration); } void EmojiColorPicker::showAnimated() { if (_ignoreShow) return; if (!isHidden() && !_hiding) { return; } _hiding = false; if (_cache.isNull()) { _cache = myGrab(this); clearSelection(); } show(); _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration); } void EmojiColorPicker::clearSelection() { _pressedSel = -1; setSelected(-1); _lastMousePos = mapToGlobal(QPoint(-10, -10)); } void EmojiColorPicker::updateSelected() { auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - st::emojiColorsPadding; if (y >= 0 && y < st::emojiPanSize.height()) { auto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding; if (x >= 0 && x < st::emojiPanSize.width()) { newSelected = 0; } else { x -= st::emojiPanSize.width() + 2 * st::emojiColorsPadding + st::emojiColorsSep; if (x >= 0 && x < st::emojiPanSize.width() * (_variants.size() - 1)) { newSelected = (x / st::emojiPanSize.width()) + 1; } } } setSelected(newSelected); } void EmojiColorPicker::setSelected(int newSelected) { if (_selected == newSelected) { return; } auto updateSelectedRect = [this] { if (_selected < 0) return; rtlupdate(st::emojiPanMargins.left() + st::emojiColorsPadding + _selected * st::emojiPanSize.width() + (_selected ? 2 * st::emojiColorsPadding + st::emojiColorsSep : 0), st::emojiPanMargins.top() + st::emojiColorsPadding, st::emojiPanSize.width(), st::emojiPanSize.height()); }; updateSelectedRect(); _selected = newSelected; updateSelectedRect(); setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); } void EmojiColorPicker::drawVariant(Painter &p, int variant) { QPoint w(st::emojiPanMargins.left() + st::emojiColorsPadding + variant * st::emojiPanSize.width() + (variant ? 2 * st::emojiColorsPadding + st::emojiColorsSep : 0), st::emojiPanMargins.top() + st::emojiColorsPadding); if (variant == _selected) { QPoint tl(w); if (rtl()) tl.setX(width() - tl.x() - st::emojiPanSize.width()); App::roundRect(p, QRect(tl, st::emojiPanSize), st::emojiPanHover, StickerHoverCorners); } auto esize = Ui::Emoji::Size(Ui::Emoji::Index() + 1); p.drawPixmapLeft(w.x() + (st::emojiPanSize.width() - (esize / cIntRetinaFactor())) / 2, w.y() + (st::emojiPanSize.height() - (esize / cIntRetinaFactor())) / 2, width(), App::emojiLarge(), QRect(_variants[variant]->x() * esize, _variants[variant]->y() * esize, esize, esize)); } class EmojiListWidget::Controller : public TWidget { public: Controller(gsl::not_null parent); private: gsl::not_null _pan; }; EmojiListWidget::Controller::Controller(gsl::not_null parent) : TWidget(parent) , _pan(parent) { } EmojiListWidget::EmojiListWidget(QWidget *parent) : Inner(parent) , _picker(this) { resize(st::emojiPanWidth - st::emojiScroll.width - st::buttonRadius, countHeight()); setMouseTracking(true); setAttribute(Qt::WA_OpaquePaintEvent); _picker->hide(); _esize = Ui::Emoji::Size(Ui::Emoji::Index() + 1); for (auto i = 0; i != kEmojiSectionCount; ++i) { _counts[i] = Ui::Emoji::GetPackCount(EmojiSectionAtIndex(i)); } _showPickerTimer.setSingleShot(true); connect(&_showPickerTimer, SIGNAL(timeout()), this, SLOT(onShowPicker())); connect(_picker, SIGNAL(emojiSelected(EmojiPtr)), this, SLOT(onColorSelected(EmojiPtr))); connect(_picker, SIGNAL(hidden()), this, SLOT(onPickerHidden())); } object_ptr EmojiListWidget::createController() { return object_ptr(this); } template bool EmojiListWidget::enumerateSections(Callback callback) const { auto info = SectionInfo(); for (auto i = 0; i != kEmojiSectionCount; ++i) { info.section = i; info.count = Ui::Emoji::GetPackCount(EmojiSectionAtIndex(i)); info.rowsCount = (info.count / kEmojiPanelPerRow) + ((info.count % kEmojiPanelPerRow) ? 1 : 0); info.rowsTop = info.top + (i == 0 ? st::emojiPanPadding : st::emojiPanHeader); info.rowsBottom = info.rowsTop + info.rowsCount * st::emojiPanSize.height(); if (!callback(info)) { return false; } info.top = info.rowsBottom; } return true; } EmojiListWidget::SectionInfo EmojiListWidget::sectionInfo(int section) const { Expects(section >= 0 && section < kEmojiSectionCount); auto result = SectionInfo(); enumerateSections([searchForSection = section, &result](const SectionInfo &info) { if (info.section == searchForSection) { result = info; return false; } return true; }); return result; } EmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset(int yOffset) const { auto result = SectionInfo(); enumerateSections([&result, yOffset](const SectionInfo &info) { if (yOffset < info.rowsBottom || info.section == kEmojiSectionCount - 1) { result = info; return false; } return true; }); return result; } int EmojiListWidget::countHeight() { return sectionInfo(kEmojiSectionCount - 1).top + st::emojiPanPadding; } void EmojiListWidget::ensureLoaded(int section) { if (!_emoji[section].isEmpty()) { return; } _emoji[section] = Ui::Emoji::GetPack(EmojiSectionAtIndex(section)); if (EmojiSectionAtIndex(section) == dbiesRecent) { return; } for (auto &emoji : _emoji[section]) { if (emoji->hasVariants()) { auto j = cEmojiVariants().constFind(emoji->nonColoredId()); if (j != cEmojiVariants().cend()) { emoji = emoji->variant(j.value()); } } } } void EmojiListWidget::paintEvent(QPaintEvent *e) { Painter p(this); QRect r = e ? e->rect() : rect(); if (r != rect()) { p.setClipRect(r); } p.fillRect(r, st::emojiPanBg); auto fromColumn = floorclamp(r.x() - st::emojiPanPadding, st::emojiPanSize.width(), 0, kEmojiPanelPerRow); auto toColumn = ceilclamp(r.x() + r.width() - st::emojiPanPadding, st::emojiPanSize.width(), 0, kEmojiPanelPerRow); if (rtl()) { qSwap(fromColumn, toColumn); fromColumn = kEmojiPanelPerRow - fromColumn; toColumn = kEmojiPanelPerRow - toColumn; } enumerateSections([this, &p, r, fromColumn, toColumn](const SectionInfo &info) { if (r.top() >= info.rowsBottom) { return true; } else if (r.top() + r.height() <= info.top) { return false; } if (info.section > 0 && r.top() < info.rowsTop) { p.setFont(st::emojiPanHeaderFont); p.setPen(st::emojiPanHeaderFg); p.drawTextLeft(st::emojiPanHeaderLeft - st::buttonRadius, info.top + st::emojiPanHeaderTop, width(), lang(LangKey(lng_emoji_category0 + info.section))); } if (r.top() + r.height() > info.rowsTop) { ensureLoaded(info.section); auto fromRow = floorclamp(r.y() - info.rowsTop, st::emojiPanSize.height(), 0, info.rowsCount); auto toRow = ceilclamp(r.y() + r.height() - info.rowsTop, st::emojiPanSize.height(), 0, info.rowsCount); for (auto i = fromRow; i < toRow; ++i) { for (auto j = fromColumn; j < toColumn; ++j) { auto index = i * kEmojiPanelPerRow + j; if (index >= info.count) break; auto selected = (!_picker->isHidden() && info.section * MatrixRowShift + index == _pickerSel) || (info.section * MatrixRowShift + index == _selected); auto w = QPoint(st::emojiPanPadding + j * st::emojiPanSize.width(), info.rowsTop + i * st::emojiPanSize.height()); if (selected) { auto tl = w; if (rtl()) tl.setX(width() - tl.x() - st::emojiPanSize.width()); App::roundRect(p, QRect(tl, st::emojiPanSize), st::emojiPanHover, StickerHoverCorners); } auto sourceRect = QRect(_emoji[info.section][index]->x() * _esize, _emoji[info.section][index]->y() * _esize, _esize, _esize); auto imageLeft = w.x() + (st::emojiPanSize.width() - (_esize / cIntRetinaFactor())) / 2; auto imageTop = w.y() + (st::emojiPanSize.height() - (_esize / cIntRetinaFactor())) / 2; p.drawPixmapLeft(imageLeft, imageTop, width(), App::emojiLarge(), sourceRect); } } } return true; }); } bool EmojiListWidget::checkPickerHide() { if (!_picker->isHidden() && _pickerSel >= 0) { _picker->hideAnimated(); _pickerSel = -1; updateSelected(); return true; } return false; } void EmojiListWidget::mousePressEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); if (checkPickerHide() || e->button() != Qt::LeftButton) { return; } _pressedSel = _selected; if (_selected >= 0) { auto section = (_selected / MatrixRowShift); auto sel = _selected % MatrixRowShift; if (section < kEmojiSectionCount && sel < _emoji[section].size() && _emoji[section][sel]->hasVariants()) { _pickerSel = _selected; setCursor(style::cur_default); if (!cEmojiVariants().contains(_emoji[section][sel]->nonColoredId())) { onShowPicker(); } else { _showPickerTimer.start(500); } } } } void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { int32 pressed = _pressedSel; _pressedSel = -1; _lastMousePos = e->globalPos(); if (!_picker->isHidden()) { if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) { return _picker->handleMouseRelease(QCursor::pos()); } else if (_pickerSel >= 0) { auto section = (_pickerSel / MatrixRowShift); auto sel = _pickerSel % MatrixRowShift; if (section < kEmojiSectionCount && sel < _emoji[section].size() && _emoji[section][sel]->hasVariants()) { if (cEmojiVariants().contains(_emoji[section][sel]->nonColoredId())) { _picker->hideAnimated(); _pickerSel = -1; } } } } updateSelected(); if (_showPickerTimer.isActive()) { _showPickerTimer.stop(); _pickerSel = -1; _picker->hide(); } if (_selected < 0 || _selected != pressed) return; if (_selected >= kEmojiSectionCount * MatrixRowShift) { return; } auto section = (_selected / MatrixRowShift); auto sel = _selected % MatrixRowShift; if (sel < _emoji[section].size()) { auto emoji = _emoji[section][sel]; if (emoji->hasVariants() && !_picker->isHidden()) return; selectEmoji(emoji); } } void EmojiListWidget::selectEmoji(EmojiPtr emoji) { auto &recent = Ui::Emoji::GetRecent(); auto i = recent.begin(), e = recent.end(); for (; i != e; ++i) { if (i->first == emoji) { ++i->second; if (i->second > 0x8000) { for (RecentEmojiPack::iterator j = recent.begin(); j != e; ++j) { if (j->second > 1) { j->second /= 2; } else { j->second = 1; } } } for (; i != recent.begin(); --i) { if ((i - 1)->second > i->second) { break; } qSwap(*i, *(i - 1)); } break; } } if (i == e) { while (recent.size() >= kEmojiPanelPerRow * kEmojiPanelRowsPerPage) recent.pop_back(); recent.push_back(qMakePair(emoji, 1)); for (i = recent.end() - 1; i != recent.begin(); --i) { if ((i - 1)->second > i->second) { break; } qSwap(*i, *(i - 1)); } } emit saveConfigDelayed(kSaveRecentEmojiTimeout); emit selected(emoji); } void EmojiListWidget::onShowPicker() { if (_pickerSel < 0) return; auto section = (_pickerSel / MatrixRowShift); auto sel = _pickerSel % MatrixRowShift; if (section < kEmojiSectionCount && sel < _emoji[section].size() && _emoji[section][sel]->hasVariants()) { _picker->showEmoji(_emoji[section][sel]); auto y = emojiRect(section, sel).y(); y -= _picker->height() - st::buttonRadius + getVisibleTop(); if (y < st::emojiPanHeader) { y += _picker->height() - st::buttonRadius + st::emojiPanSize.height() - st::buttonRadius; } auto xmax = width() - _picker->width(); auto coef = float64(sel % kEmojiPanelPerRow) / float64(kEmojiPanelPerRow - 1); if (rtl()) coef = 1. - coef; _picker->move(qRound(xmax * coef), y); emit disableScroll(true); } } void EmojiListWidget::onPickerHidden() { _pickerSel = -1; update(); emit disableScroll(false); _lastMousePos = QCursor::pos(); updateSelected(); } QRect EmojiListWidget::emojiRect(int section, int sel) { auto info = sectionInfo(section); auto countTillItem = (sel - (sel % kEmojiPanelPerRow)); auto rowsToSkip = (countTillItem / kEmojiPanelPerRow) + ((countTillItem % kEmojiPanelPerRow) ? 1 : 0); auto x = st::emojiPanPadding + ((sel % kEmojiPanelPerRow) * st::emojiPanSize.width()); auto y = info.rowsTop + rowsToSkip * st::emojiPanSize.height(); return QRect(x, y, st::emojiPanSize.width(), st::emojiPanSize.height()); } void EmojiListWidget::onColorSelected(EmojiPtr emoji) { if (emoji->hasVariants()) { cRefEmojiVariants().insert(emoji->nonColoredId(), emoji->variantIndex(emoji)); } if (_pickerSel >= 0) { auto section = (_pickerSel / MatrixRowShift); auto sel = _pickerSel % MatrixRowShift; if (section >= 0 && section < kEmojiSectionCount) { _emoji[section][sel] = emoji; rtlupdate(emojiRect(section, sel)); } } selectEmoji(emoji); _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() { _lastMousePos = mapToGlobal(QPoint(-10, -10)); _pressedSel = -1; setSelected(-1); } DBIEmojiSection EmojiListWidget::currentSection(int yOffset) const { return EmojiSectionAtIndex(sectionInfoByOffset(yOffset).section); } void EmojiListWidget::hideFinish(bool completely) { if (!_picker->isHidden()) { _picker->hideFast(); _pickerSel = -1; } clearSelection(); } void EmojiListWidget::refreshRecent() { clearSelection(); _counts[0] = Ui::Emoji::GetPackCount(dbiesRecent); _emoji[0] = Ui::Emoji::GetPack(dbiesRecent); auto h = countHeight(); if (h != height()) { resize(width(), h); update(); } } bool EmojiListWidget::event(QEvent *e) { if (e->type() == QEvent::ParentChange) { if (_picker->parentWidget() != parentWidget()) { _picker->setParent(parentWidget()); } _picker->raise(); } return Inner::event(e); } void EmojiListWidget::updateSelected() { if (_pressedSel >= 0 || _pickerSel >= 0) return; auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); auto info = sectionInfoByOffset(p.y()); if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) { auto sx = (rtl() ? width() - p.x() : p.x()) - st::emojiPanPadding; if (sx >= 0 && sx < kEmojiPanelPerRow * st::emojiPanSize.width()) { newSelected = qFloor((p.y() - info.rowsTop) / st::emojiPanSize.height()) * kEmojiPanelPerRow + qFloor(sx / st::emojiPanSize.width()); if (newSelected >= _emoji[info.section].size()) { newSelected = -1; } else { newSelected += info.section * MatrixRowShift; } } } setSelected(newSelected); } void EmojiListWidget::setSelected(int newSelected) { if (_selected == newSelected) { return; } auto updateSelected = [this]() { if (_selected < 0) return; rtlupdate(emojiRect(_selected / MatrixRowShift, _selected % MatrixRowShift)); }; updateSelected(); _selected = newSelected; updateSelected(); setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); if (_selected >= 0 && !_picker->isHidden()) { if (_selected != _pickerSel) { _picker->hideAnimated(); } else { _picker->showAnimated(); } } } void EmojiListWidget::showEmojiSection(DBIEmojiSection section) { clearSelection(); refreshRecent(); auto y = 0; enumerateSections([&y, sectionForSearch = section](const SectionInfo &info) { if (EmojiSectionAtIndex(info.section) == sectionForSearch) { y = info.top; return false; } return true; }); emit scrollToY(y); _lastMousePos = QCursor::pos(); update(); } } // namespace ChatHelpers