/* 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 "ui/special_buttons.h" #include "styles/style_boxes.h" #include "styles/style_history.h" #include "dialogs/dialogs_layout.h" #include "ui/effects/ripple_animation.h" #include "data/data_photo.h" #include "core/file_utilities.h" #include "boxes/photo_crop_box.h" #include "boxes/confirm_box.h" #include "window/window_controller.h" #include "lang/lang_keys.h" #include "auth_session.h" #include "messenger.h" #include "observer_peer.h" namespace Ui { namespace { constexpr int kWideScale = 5; template QPixmap CreateSquarePixmap(int width, Callback &&paintCallback) { auto size = QSize(width, width) * cIntRetinaFactor(); auto image = QImage(size, QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(cRetinaFactor()); image.fill(Qt::transparent); { Painter p(&image); paintCallback(p); } return App::pixmapFromImageInPlace(std::move(image)); }; template void SuggestPhoto( const QImage &image, PeerId peerForCrop, Callback &&callback) { auto badAspect = [](int a, int b) { return (a >= 10 * b); }; if (image.isNull() || badAspect(image.width(), image.height()) || badAspect(image.height(), image.width())) { Ui::show( Box(lang(lng_bad_photo)), LayerOption::KeepOther); return; } auto box = Ui::show( Box(image, peerForCrop), LayerOption::KeepOther); box->ready() | rpl::start_with_next( std::forward(callback), box->lifetime()); } template void SuggestPhotoFile( const FileDialog::OpenResult &result, PeerId peerForCrop, Callback &&callback) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { return; } auto image = [&] { if (!result.remoteContent.isEmpty()) { return App::readImage(result.remoteContent); } else if (!result.paths.isEmpty()) { return App::readImage(result.paths.front()); } return QImage(); }(); SuggestPhoto( image, peerForCrop, std::forward(callback)); } template void ShowChoosePhotoBox(PeerId peerForCrop, Callback &&callback) { auto imgExtensions = cImgExtensions(); auto filter = qsl("Image files (*") + imgExtensions.join(qsl(" *")) + qsl(");;") + FileDialog::AllFilesFilter(); auto handleChosenPhoto = [ peerForCrop, callback = std::forward(callback) ](auto &&result) mutable { SuggestPhotoFile(result, peerForCrop, std::move(callback)); }; FileDialog::GetOpenPath( lang(lng_choose_image), filter, std::move(handleChosenPhoto)); } } // namespace HistoryDownButton::HistoryDownButton(QWidget *parent, const style::TwoIconButton &st) : RippleButton(parent, st.ripple) , _st(st) { resize(_st.width, _st.height); setCursor(style::cur_pointer); hide(); } QImage HistoryDownButton::prepareRippleMask() const { return Ui::RippleAnimation::ellipseMask(QSize(_st.rippleAreaSize, _st.rippleAreaSize)); } QPoint HistoryDownButton::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition; } void HistoryDownButton::paintEvent(QPaintEvent *e) { Painter p(this); auto ms = getms(); auto over = isOver(); auto down = isDown(); ((over || down) ? _st.iconBelowOver : _st.iconBelow).paint(p, _st.iconPosition, width()); paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), ms); ((over || down) ? _st.iconAboveOver : _st.iconAbove).paint(p, _st.iconPosition, width()); if (_unreadCount > 0) { auto unreadString = QString::number(_unreadCount); if (unreadString.size() > 4) { unreadString = qsl("..") + unreadString.mid(unreadString.size() - 4); } Dialogs::Layout::UnreadBadgeStyle st; st.align = style::al_center; st.font = st::historyToDownBadgeFont; st.size = st::historyToDownBadgeSize; st.sizeId = Dialogs::Layout::UnreadBadgeInHistoryToDown; Dialogs::Layout::paintUnreadCount(p, unreadString, width(), 0, st, nullptr); } } void HistoryDownButton::setUnreadCount(int unreadCount) { if (_unreadCount != unreadCount) { _unreadCount = unreadCount; update(); } } EmojiButton::EmojiButton(QWidget *parent, const style::IconButton &st) : RippleButton(parent, st.ripple) , _st(st) , _a_loading(animation(this, &EmojiButton::step_loading)) { resize(_st.width, _st.height); setCursor(style::cur_pointer); } void EmojiButton::paintEvent(QPaintEvent *e) { Painter p(this); auto ms = getms(); p.fillRect(e->rect(), st::historyComposeAreaBg); paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), ms, _rippleOverride ? &(*_rippleOverride)->c : nullptr); auto loading = a_loading.current(ms, _loading ? 1 : 0); p.setOpacity(1 - loading); auto over = isOver(); auto icon = _iconOverride ? _iconOverride : &(over ? _st.iconOver : _st.icon); icon->paint(p, _st.iconPosition, width()); p.setOpacity(1.); auto pen = _colorOverride ? (*_colorOverride)->p : (over ? st::historyEmojiCircleFgOver : st::historyEmojiCircleFg)->p; pen.setWidth(st::historyEmojiCircleLine); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); p.setBrush(Qt::NoBrush); PainterHighQualityEnabler hq(p); QRect inner(QPoint((width() - st::historyEmojiCircle.width()) / 2, st::historyEmojiCircleTop), st::historyEmojiCircle); if (loading > 0) { int32 full = FullArcLength; int32 start = qRound(full * float64(ms % st::historyEmojiCirclePeriod) / st::historyEmojiCirclePeriod), part = qRound(loading * full / st::historyEmojiCirclePart); p.drawArc(inner, start, full - part); } else { p.drawEllipse(inner); } } void EmojiButton::setLoading(bool loading) { if (_loading != loading) { _loading = loading; auto from = loading ? 0. : 1., to = loading ? 1. : 0.; a_loading.start([this] { update(); }, from, to, st::historyEmojiCircleDuration); if (loading) { _a_loading.start(); } else { _a_loading.stop(); } } } void EmojiButton::setColorOverrides(const style::icon *iconOverride, const style::color *colorOverride, const style::color *rippleOverride) { _iconOverride = iconOverride; _colorOverride = colorOverride; _rippleOverride = rippleOverride; update(); } void EmojiButton::onStateChanged(State was, StateChangeSource source) { RippleButton::onStateChanged(was, source); auto wasOver = static_cast(was & StateFlag::Over); if (isOver() != wasOver) { update(); } } QPoint EmojiButton::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition; } QImage EmojiButton::prepareRippleMask() const { return RippleAnimation::ellipseMask(QSize(_st.rippleAreaSize, _st.rippleAreaSize)); } SendButton::SendButton(QWidget *parent) : RippleButton(parent, st::historyReplyCancel.ripple) { resize(st::historySendSize); } void SendButton::setType(Type type) { if (_type != type) { _contentFrom = grabContent(); _type = type; _a_typeChanged.finish(); _contentTo = grabContent(); _a_typeChanged.start([this] { update(); }, 0., 1., st::historyRecordVoiceDuration); update(); } if (_type != Type::Record) { _recordActive = false; _a_recordActive.finish(); } } void SendButton::setRecordActive(bool recordActive) { if (_recordActive != recordActive) { _recordActive = recordActive; _a_recordActive.start([this] { recordAnimationCallback(); }, _recordActive ? 0. : 1., _recordActive ? 1. : 0, st::historyRecordVoiceDuration); update(); } } void SendButton::finishAnimating() { _a_typeChanged.finish(); _a_recordActive.finish(); update(); } void SendButton::mouseMoveEvent(QMouseEvent *e) { AbstractButton::mouseMoveEvent(e); if (_recording) { if (_recordUpdateCallback) { _recordUpdateCallback(e->globalPos()); } } } void SendButton::paintEvent(QPaintEvent *e) { Painter p(this); auto ms = getms(); auto over = (isDown() || isOver()); auto changed = _a_typeChanged.current(ms, 1.); if (changed < 1.) { PainterHighQualityEnabler hq(p); p.setOpacity(1. - changed); auto targetRect = QRect((1 - kWideScale) / 2 * width(), (1 - kWideScale) / 2 * height(), kWideScale * width(), kWideScale * height()); auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * width(), changed); auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * height(), changed); p.drawPixmap(targetRect.marginsAdded(QMargins(hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight)), _contentFrom); p.setOpacity(changed); auto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed); auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * height(), 0, changed); p.drawPixmap(targetRect.marginsAdded(QMargins(shownWidth, shownHeight, shownWidth, shownHeight)), _contentTo); } else if (_type == Type::Record) { auto recordActive = recordActiveRatio(); auto rippleColor = anim::color(st::historyAttachEmoji.ripple.color, st::historyRecordVoiceRippleBgActive, recordActive); paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), ms, &rippleColor); auto fastIcon = [recordActive, over, this] { if (recordActive == 1.) { return &st::historyRecordVoiceActive; } else if (over) { return &st::historyRecordVoiceOver; } return &st::historyRecordVoice; }; fastIcon()->paintInCenter(p, rect()); if (recordActive > 0. && recordActive < 1.) { p.setOpacity(recordActive); st::historyRecordVoiceActive.paintInCenter(p, rect()); p.setOpacity(1.); } } else if (_type == Type::Save) { auto &saveIcon = over ? st::historyEditSaveIconOver : st::historyEditSaveIcon; saveIcon.paint(p, st::historySendIconPosition, width()); } else if (_type == Type::Cancel) { paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), ms); auto &cancelIcon = over ? st::historyReplyCancelIconOver : st::historyReplyCancelIcon; cancelIcon.paintInCenter(p, rect()); } else { auto &sendIcon = over ? st::historySendIconOver : st::historySendIcon; sendIcon.paint(p, st::historySendIconPosition, width()); } } void SendButton::onStateChanged(State was, StateChangeSource source) { RippleButton::onStateChanged(was, source); auto down = (state() & StateFlag::Down); if ((was & StateFlag::Down) != down) { if (down) { if (_type == Type::Record) { _recording = true; if (_recordStartCallback) { _recordStartCallback(); } } } else if (_recording) { _recording = false; if (_recordStopCallback) { _recordStopCallback(_recordActive); } } } } QPixmap SendButton::grabContent() { auto result = QImage(kWideScale * size() * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); result.setDevicePixelRatio(cRetinaFactor()); result.fill(Qt::transparent); { Painter p(&result); p.drawPixmap((kWideScale - 1) / 2 * width(), (kWideScale - 1) / 2 * height(), myGrab(this)); } return App::pixmapFromImageInPlace(std::move(result)); } QImage SendButton::prepareRippleMask() const { auto size = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaSize : st::historyReplyCancel.rippleAreaSize; return Ui::RippleAnimation::ellipseMask(QSize(size, size)); } QPoint SendButton::prepareRippleStartPosition() const { auto real = mapFromGlobal(QCursor::pos()); auto size = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaSize : st::historyReplyCancel.rippleAreaSize; auto y = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaPosition.y() : (height() - st::historyReplyCancel.rippleAreaSize) / 2; return real - QPoint((width() - size) / 2, y); } void SendButton::recordAnimationCallback() { update(); if (_recordAnimationCallback) { _recordAnimationCallback(); } } UserpicButton::UserpicButton( QWidget *parent, PeerId peerForCrop, Role role, const style::UserpicButton &st) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _peerForCrop(peerForCrop) , _role(role) { Expects(_role == Role::ChangePhoto); _waiting = false; prepare(); } UserpicButton::UserpicButton( QWidget *parent, not_null controller, not_null peer, Role role, const style::UserpicButton &st) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(controller) , _peer(peer) , _peerForCrop(_peer->id) , _role(role) { processPeerPhoto(); prepare(); setupPeerViewers(); } void UserpicButton::prepare() { resize(_st.size); _notShownYet = _waiting; if (!_waiting) { prepareUserpicPixmap(); } setClickHandlerByRole(); } void UserpicButton::setClickHandlerByRole() { switch (_role) { case Role::ChangePhoto: addClickHandler(App::LambdaDelayed( _st.changeButton.ripple.hideDuration, this, [this] { changePhotoLazy(); })); break; case Role::OpenPhoto: addClickHandler([this] { openPeerPhoto(); }); break; case Role::OpenProfile: addClickHandler([this] { Expects(_controller != nullptr); _controller->showPeerInfo(_peer); }); break; } } void UserpicButton::changePhotoLazy() { auto callback = base::lambda_guarded( this, [this](QImage &&image) { setImage(std::move(image)); }); ShowChoosePhotoBox(_peerForCrop, std::move(callback)); } void UserpicButton::uploadNewPeerPhoto() { auto callback = base::lambda_guarded( this, [this](QImage &&image) { Messenger::Instance().uploadProfilePhoto( std::move(image), _peer->id ); }); ShowChoosePhotoBox(_peerForCrop, std::move(callback)); } void UserpicButton::openPeerPhoto() { Expects(_peer != nullptr); Expects(_controller != nullptr); if (_changeOverlayEnabled && _cursorInChangeOverlay) { uploadNewPeerPhoto(); return; } auto id = _peer->photoId; if (!id || id == UnknownPeerPhotoId) { return; } auto photo = App::photo(id); if (photo->date) { Messenger::Instance().showPhoto(photo, _peer); } } void UserpicButton::setupPeerViewers() { Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::PhotoChanged) | rpl::start_with_next([this] { processNewPeerPhoto(); update(); }, lifetime()); base::ObservableViewer(Auth().downloaderTaskFinished()) | rpl::start_with_next([this] { if (_waiting && _peer->userpicLoaded()) { _waiting = false; startNewPhotoShowing(); } }, lifetime()); } void UserpicButton::paintEvent(QPaintEvent *e) { Painter p(this); if (!_waiting && _notShownYet) { _notShownYet = false; startAnimation(); } auto photoPosition = countPhotoPosition(); auto photoLeft = photoPosition.x(); auto photoTop = photoPosition.y(); auto ms = getms(); if (_a_appearance.animating(ms)) { p.drawPixmapLeft(photoPosition, width(), _oldUserpic); p.setOpacity(_a_appearance.current()); } p.drawPixmapLeft(photoPosition, width(), _userpic); if (_role == Role::ChangePhoto) { auto over = isOver() || isDown(); if (over) { PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(_userpicHasImage ? st::msgDateImgBg : _st.changeButton.textBgOver); p.drawEllipse( photoLeft, photoTop, _st.photoSize, _st.photoSize); } paintRipple( p, photoLeft, photoTop, ms, _userpicHasImage ? &st::shadowFg->c : &_st.changeButton.ripple.color->c); if (over || !_userpicHasImage) { auto iconLeft = (_st.changeIconPosition.x() < 0) ? (_st.photoSize - _st.changeIcon.width()) / 2 : _st.changeIconPosition.x(); auto iconTop = (_st.changeIconPosition.y() < 0) ? (_st.photoSize - _st.changeIcon.height()) / 2 : _st.changeIconPosition.y(); _st.changeIcon.paint( p, photoLeft + iconLeft, photoTop + iconTop, width()); } } else if (_changeOverlayEnabled) { auto current = _changeOverlayShown.current( ms, (isOver() || isDown()) ? 1. : 0.); auto barHeight = anim::interpolate( 0, _st.uploadHeight, current); if (barHeight > 0) { auto barLeft = photoLeft; auto barTop = photoTop + _st.photoSize - barHeight; auto rect = QRect( barLeft, barTop, _st.photoSize, barHeight); p.setClipRect(rect); { PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.setBrush(_st.uploadBg); p.drawEllipse( photoLeft, photoTop, _st.photoSize, _st.photoSize); } auto iconLeft = (_st.uploadIconPosition.x() < 0) ? (_st.photoSize - _st.uploadIcon.width()) / 2 : _st.uploadIconPosition.x(); auto iconTop = (_st.uploadIconPosition.y() < 0) ? (_st.uploadHeight - _st.uploadIcon.height()) / 2 : _st.uploadIconPosition.y(); if (iconTop < barHeight) { _st.uploadIcon.paint( p, barLeft + iconLeft, barTop + iconTop, width()); } } } } QPoint UserpicButton::countPhotoPosition() const { auto photoLeft = (_st.photoPosition.x() < 0) ? (width() - _st.photoSize) / 2 : _st.photoPosition.x(); auto photoTop = (_st.photoPosition.y() < 0) ? (height() - _st.photoSize) / 2 : _st.photoPosition.y(); return { photoLeft, photoTop }; } QImage UserpicButton::prepareRippleMask() const { return Ui::RippleAnimation::ellipseMask(QSize( _st.photoSize, _st.photoSize)); } QPoint UserpicButton::prepareRippleStartPosition() const { return (_role == Role::ChangePhoto) ? mapFromGlobal(QCursor::pos()) - countPhotoPosition() : DisabledRippleStartPosition(); } void UserpicButton::processPeerPhoto() { Expects(_peer != nullptr); auto hasPhoto = (_peer->photoId && _peer->photoId != UnknownPeerPhotoId); _waiting = !_peer->userpicLoaded(); if (_waiting) { _peer->loadUserpic(true); } if (_role == Role::OpenPhoto) { auto id = _peer->photoId; if (id == UnknownPeerPhotoId) { _peer->updateFullForced(); } _canOpenPhoto = (id != 0 && id != UnknownPeerPhotoId); updateCursor(); } } void UserpicButton::updateCursor() { Expects(_role == Role::OpenPhoto); auto pointer = _canOpenPhoto || (_changeOverlayEnabled && _cursorInChangeOverlay); setPointerCursor(pointer); } void UserpicButton::mouseMoveEvent(QMouseEvent *e) { RippleButton::mouseMoveEvent(e); if (_role == Role::OpenPhoto) { updateCursorInChangeOverlay(e->pos()); } } void UserpicButton::updateCursorInChangeOverlay(QPoint localPos) { auto photoPosition = countPhotoPosition(); auto overlayRect = QRect( photoPosition.x(), photoPosition.y() + _st.photoSize - _st.uploadHeight, _st.photoSize, _st.uploadHeight); auto inOverlay = overlayRect.contains(localPos); setCursorInChangeOverlay(inOverlay); } void UserpicButton::leaveEventHook(QEvent *e) { if (_role == Role::OpenPhoto) { setCursorInChangeOverlay(false); } return RippleButton::leaveEventHook(e); } void UserpicButton::setCursorInChangeOverlay(bool inOverlay) { Expects(_role == Role::OpenPhoto); if (_cursorInChangeOverlay != inOverlay) { _cursorInChangeOverlay = inOverlay; updateCursor(); } } void UserpicButton::processNewPeerPhoto() { if (_userpicCustom) { return; } processPeerPhoto(); if (!_waiting) { grabOldUserpic(); startNewPhotoShowing(); } } void UserpicButton::grabOldUserpic() { auto photoRect = QRect( countPhotoPosition(), QSize(_st.photoSize, _st.photoSize) ); _oldUserpic = myGrab(this, photoRect); } void UserpicButton::startNewPhotoShowing() { auto oldUniqueKey = _userpicUniqueKey; prepareUserpicPixmap(); if (_notShownYet) { return; } if (oldUniqueKey != _userpicUniqueKey || _a_appearance.animating()) { startAnimation(); } update(); } void UserpicButton::startAnimation() { _a_appearance.finish(); _a_appearance.start([this] { update(); }, 0, 1, _st.duration); } void UserpicButton::switchChangePhotoOverlay(bool enabled) { Expects(_role == Role::OpenPhoto); if (_changeOverlayEnabled != enabled) { _changeOverlayEnabled = enabled; if (enabled) { if (isOver()) { startChangeOverlayAnimation(); } updateCursorInChangeOverlay( mapFromGlobal(QCursor::pos())); } else { _changeOverlayShown.finish(); update(); } } } void UserpicButton::startChangeOverlayAnimation() { auto over = isOver() || isDown(); _changeOverlayShown.start( [this] { update(); }, over ? 0. : 1., over ? 1. : 0., st::slideWrapDuration); update(); } void UserpicButton::onStateChanged( State was, StateChangeSource source) { RippleButton::onStateChanged(was, source); if (_changeOverlayEnabled) { auto mask = (StateFlag::Over | StateFlag::Down); auto wasOver = (was & mask) != 0; auto nowOver = (state() & mask) != 0; if (wasOver != nowOver) { startChangeOverlayAnimation(); } } } void UserpicButton::setImage(QImage &&image) { grabOldUserpic(); auto size = QSize(_st.photoSize, _st.photoSize); auto small = image.scaled( size * cIntRetinaFactor(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); Images::prepareCircle(small); _userpic = App::pixmapFromImageInPlace(std::move(small)); _userpic.setDevicePixelRatio(cRetinaFactor()); _userpicCustom = _userpicHasImage = true; _result = std::move(image); startNewPhotoShowing(); } void UserpicButton::prepareUserpicPixmap() { if (_userpicCustom) { return; } auto size = _st.photoSize; auto paintButton = [&](Painter &p, const style::color &color) { PainterHighQualityEnabler hq(p); p.setBrush(color); p.setPen(Qt::NoPen); p.drawEllipse(0, 0, size, size); }; _userpicHasImage = _peer ? (_peer->currentUserpic() || _role != Role::ChangePhoto) : false; _userpic = CreateSquarePixmap(size, [&](Painter &p) { if (_userpicHasImage) { _peer->paintUserpic(p, 0, 0, _st.photoSize); } else { paintButton(p, _st.changeButton.textBg); } }); _userpicUniqueKey = _userpicHasImage ? _peer->userpicUniqueKey() : StorageKey(); } } // namespace Ui