tdesktop/Telegram/SourceFiles/ui/special_buttons.cpp

839 lines
20 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 "ui/special_buttons.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "dialogs/dialogs_layout.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/radial_animation.h"
#include "ui/image/image_prepare.h"
#include "ui/empty_userpic.h"
#include "ui/ui_utility.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_channel.h"
#include "data/data_cloud_file.h"
#include "data/data_changes.h"
#include "data/data_user.h"
#include "data/data_streaming.h"
#include "data/data_file_origin.h"
#include "history/history.h"
#include "core/file_utilities.h"
#include "core/application.h"
#include "boxes/confirm_box.h"
#include "editor/photo_editor_layer_widget.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "facades.h"
#include "app.h"
namespace Ui {
namespace {
constexpr auto kAnimationDuration = crl::time(120);
QString CropTitle(not_null<PeerData*> peer) {
if (peer->isChat() || peer->isMegagroup()) {
return tr::lng_create_group_crop(tr::now);
} else if (peer->isChannel()) {
return tr::lng_create_channel_crop(tr::now);
} else {
return tr::lng_settings_crop_profile(tr::now);
}
}
template <typename Callback>
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));
};
} // 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);
const auto over = isOver();
const auto down = isDown();
((over || down) ? _st.iconBelowOver : _st.iconBelow).paint(p, _st.iconPosition, width());
paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y());
((over || down) ? _st.iconAboveOver : _st.iconAbove).paint(p, _st.iconPosition, width());
if (_unreadCount > 0) {
auto unreadString = QString::number(_unreadCount);
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, 4);
}
}
void HistoryDownButton::setUnreadCount(int unreadCount) {
if (_unreadCount != unreadCount) {
_unreadCount = unreadCount;
update();
}
}
UserpicButton::UserpicButton(
QWidget *parent,
not_null<Window::Controller*> window,
not_null<PeerData*> peer,
Role role,
const style::UserpicButton &st)
: RippleButton(parent, st.changeButton.ripple)
, _st(st)
, _controller(window->sessionController())
, _window(window)
, _peer(peer)
, _cropTitle(CropTitle(peer))
, _role(role) {
Expects(_role == Role::ChangePhoto);
_waiting = false;
prepare();
}
UserpicButton::UserpicButton(
QWidget *parent,
not_null<Window::Controller*> window,
const QString &cropTitle,
Role role,
const style::UserpicButton &st)
: RippleButton(parent, st.changeButton.ripple)
, _st(st)
, _controller(window->sessionController())
, _window(window)
, _cropTitle(cropTitle)
, _role(role) {
Expects(_role == Role::ChangePhoto);
_waiting = false;
prepare();
}
UserpicButton::UserpicButton(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Role role,
const style::UserpicButton &st)
: RippleButton(parent, st.changeButton.ripple)
, _st(st)
, _controller(controller)
, _window(&controller->window())
, _peer(peer)
, _cropTitle(CropTitle(_peer))
, _role(role) {
processPeerPhoto();
prepare();
setupPeerViewers();
}
UserpicButton::UserpicButton(
QWidget *parent,
not_null<PeerData*> peer,
Role role,
const style::UserpicButton &st)
: RippleButton(parent, st.changeButton.ripple)
, _st(st)
, _peer(peer)
, _cropTitle(CropTitle(_peer))
, _role(role) {
Expects(_role != Role::OpenProfile && _role != Role::OpenPhoto);
_waiting = false;
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,
[=] { changePhotoLocally(); }));
break;
case Role::OpenPhoto:
addClickHandler([=] {
openPeerPhoto();
});
break;
case Role::OpenProfile:
addClickHandler([this] {
Expects(_controller != nullptr);
_controller->showPeerInfo(_peer);
});
break;
}
}
void UserpicButton::changePhotoLocally(bool requestToUpload) {
if (!_window) {
return;
}
auto callback = [=](QImage &&image) {
setImage(std::move(image));
if (requestToUpload) {
_uploadPhotoRequests.fire({});
}
};
Editor::PrepareProfilePhoto(
this,
_window,
std::move(callback));
}
void UserpicButton::openPeerPhoto() {
Expects(_peer != nullptr);
Expects(_controller != nullptr);
if (_changeOverlayEnabled && _cursorInChangeOverlay) {
changePhotoLocally(true);
return;
}
const auto id = _peer->userpicPhotoId();
if (!id) {
return;
}
const auto photo = _peer->owner().photo(id);
if (photo->date && _controller) {
_controller->openPhoto(photo, _peer);
}
}
void UserpicButton::setupPeerViewers() {
_peer->session().changes().peerUpdates(
_peer,
Data::PeerUpdate::Flag::Photo
) | rpl::start_with_next([=] {
processNewPeerPhoto();
update();
}, lifetime());
_peer->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _waiting;
}) | rpl::start_with_next([=] {
if (!_userpicView || _userpicView->image()) {
_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();
if (showSavedMessages()) {
Ui::EmptyUserpic::PaintSavedMessages(
p,
photoPosition.x(),
photoPosition.y(),
width(),
_st.photoSize);
} else if (showRepliesMessages()) {
Ui::EmptyUserpic::PaintRepliesMessages(
p,
photoPosition.x(),
photoPosition.y(),
width(),
_st.photoSize);
} else {
if (_a_appearance.animating()) {
p.drawPixmapLeft(photoPosition, width(), _oldUserpic);
p.setOpacity(_a_appearance.value(1.));
}
paintUserpicFrame(p, photoPosition);
}
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,
_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.value(
(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());
}
}
}
}
void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) {
checkStreamedIsStarted();
if (_streamed
&& _streamed->player().ready()
&& !_streamed->player().videoSize().isEmpty()) {
const auto paused = _controller
? _controller->isGifPausedAtLeastFor(
Window::GifPauseReason::RoundPlaying)
: false;
auto request = Media::Streaming::FrameRequest();
auto size = QSize{ _st.photoSize, _st.photoSize };
request.outer = size * cIntRetinaFactor();
request.resize = size * cIntRetinaFactor();
request.radius = ImageRoundRadius::Ellipse;
p.drawImage(QRect(photoPosition, size), _streamed->frame(request));
if (!paused) {
_streamed->markFrameShown();
}
} else {
p.drawPixmapLeft(photoPosition, width(), _userpic);
}
}
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);
_userpicView = _peer->createUserpicView();
_waiting = _userpicView && !_userpicView->image();
if (_waiting) {
_peer->loadUserpic();
}
if (_role == Role::OpenPhoto) {
if (_peer->userpicPhotoUnknown()) {
_peer->updateFullForced();
}
_canOpenPhoto = (_peer->userpicPhotoId() != 0);
updateCursor();
updateVideo();
}
}
void UserpicButton::updateCursor() {
Expects(_role == Role::OpenPhoto);
auto pointer = _canOpenPhoto
|| (_changeOverlayEnabled && _cursorInChangeOverlay);
setPointerCursor(pointer);
}
bool UserpicButton::createStreamingObjects(not_null<PhotoData*> photo) {
Expects(_peer != nullptr);
using namespace Media::Streaming;
const auto origin = _peer->isUser()
? Data::FileOriginUserPhoto(peerToUser(_peer->id), photo->id)
: Data::FileOrigin(Data::FileOriginPeerPhoto(_peer->id));
_streamed = std::make_unique<Instance>(
photo->owner().streaming().sharedDocument(photo, origin),
nullptr);
_streamed->lockPlayer();
_streamed->player().updates(
) | rpl::start_with_next_error([=](Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Error &&error) {
handleStreamingError(std::move(error));
}, _streamed->lifetime());
if (_streamed->ready()) {
streamingReady(base::duplicate(_streamed->info()));
}
if (!_streamed->valid()) {
clearStreaming();
return false;
}
return true;
}
void UserpicButton::clearStreaming() {
_streamed = nullptr;
_streamedPhoto = nullptr;
}
void UserpicButton::handleStreamingUpdate(Media::Streaming::Update &&update) {
using namespace Media::Streaming;
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [&](const PreloadedVideo &update) {
}, [&](const UpdateVideo &update) {
this->update();
}, [&](const PreloadedAudio &update) {
}, [&](const UpdateAudio &update) {
}, [&](const WaitingForData &update) {
}, [&](MutedByOther) {
}, [&](Finished) {
});
}
void UserpicButton::handleStreamingError(Media::Streaming::Error &&error) {
Expects(_peer != nullptr);
_streamedPhoto->setVideoPlaybackFailed();
_streamedPhoto = nullptr;
_streamed = nullptr;
}
void UserpicButton::streamingReady(Media::Streaming::Information &&info) {
update();
}
void UserpicButton::updateVideo() {
Expects(_role == Role::OpenPhoto);
const auto id = _peer->userpicPhotoId();
if (!id) {
clearStreaming();
return;
}
const auto photo = _peer->owner().photo(id);
if (!photo->date || !photo->videoCanBePlayed()) {
clearStreaming();
return;
} else if (_streamed && _streamedPhoto == photo) {
return;
}
if (!createStreamingObjects(photo)) {
photo->setVideoPlaybackFailed();
return;
}
_streamedPhoto = photo;
checkStreamedIsStarted();
}
void UserpicButton::checkStreamedIsStarted() {
Expects(!_streamed || _streamedPhoto);
if (!_streamed) {
return;
} else if (_streamed->paused()) {
_streamed->resume();
}
if (_streamed && !_streamed->active() && !_streamed->failed()) {
const auto position = _streamedPhoto->videoStartPosition();
auto options = Media::Streaming::PlaybackOptions();
options.position = position;
options.mode = Media::Streaming::Mode::Video;
options.loop = true;
_streamed->play(options);
}
}
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 = GrabWidget(this, photoRect);
}
void UserpicButton::startNewPhotoShowing() {
auto oldUniqueKey = _userpicUniqueKey;
prepareUserpicPixmap();
update();
if (_notShownYet) {
return;
}
if (oldUniqueKey != _userpicUniqueKey
|| _a_appearance.animating()) {
startAnimation();
}
}
void UserpicButton::startAnimation() {
_a_appearance.stop();
_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.stop();
update();
}
}
}
void UserpicButton::showSavedMessagesOnSelf(bool enabled) {
if (_showSavedMessagesOnSelf != enabled) {
_showSavedMessagesOnSelf = enabled;
update();
}
}
bool UserpicButton::showSavedMessages() const {
return _showSavedMessagesOnSelf && _peer && _peer->isSelf();
}
bool UserpicButton::showRepliesMessages() const {
return _showSavedMessagesOnSelf && _peer && _peer->isRepliesChat();
}
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(_userpicView) || _role != Role::ChangePhoto)
: false;
_userpic = CreateSquarePixmap(size, [&](Painter &p) {
if (_userpicHasImage) {
_peer->paintUserpic(p, _userpicView, 0, 0, _st.photoSize);
} else {
paintButton(p, _st.changeButton.textBg);
}
});
_userpicUniqueKey = _userpicHasImage
? _peer->userpicUniqueKey(_userpicView)
: InMemoryKey();
}
rpl::producer<> UserpicButton::uploadPhotoRequests() const {
return _uploadPhotoRequests.events();
}
SilentToggle::SilentToggle(QWidget *parent, not_null<ChannelData*> channel)
: RippleButton(parent, st::historySilentToggle.ripple)
, _st(st::historySilentToggle)
, _colorOver(st::historyComposeIconFgOver->c)
, _channel(channel)
, _checked(channel->owner().notifySilentPosts(_channel))
, _crossLine(st::historySilentToggleCrossLine) {
Expects(!channel->owner().notifySilentPostsUnknown(_channel));
resize(_st.width, _st.height);
style::PaletteChanged(
) | rpl::start_with_next([=] {
_crossLine.invalidate();
}, lifetime());
paintRequest(
) | rpl::start_with_next([=](const QRect &clip) {
Painter p(this);
paintRipple(p, _st.rippleAreaPosition, nullptr);
_crossLine.paint(
p,
(width() - _st.icon.width()) / 2,
(height() - _st.icon.height()) / 2,
_crossLineAnimation.value(_checked ? 1. : 0.),
// Since buttons of the compose controls have no duration
// for the over animation, we can skip this animation here.
isOver()
? std::make_optional<QColor>(_colorOver)
: std::nullopt);
}, lifetime());
setMouseTracking(true);
}
void SilentToggle::mouseMoveEvent(QMouseEvent *e) {
RippleButton::mouseMoveEvent(e);
if (rect().contains(e->pos())) {
Ui::Tooltip::Show(1000, this);
} else {
Ui::Tooltip::Hide();
}
}
void SilentToggle::setChecked(bool checked) {
if (_checked != checked) {
_checked = checked;
_crossLineAnimation.start(
[=] { update(); },
_checked ? 0. : 1.,
_checked ? 1. : 0.,
kAnimationDuration);
}
}
void SilentToggle::leaveEventHook(QEvent *e) {
RippleButton::leaveEventHook(e);
Ui::Tooltip::Hide();
}
void SilentToggle::mouseReleaseEvent(QMouseEvent *e) {
setChecked(!_checked);
RippleButton::mouseReleaseEvent(e);
Ui::Tooltip::Show(0, this);
_channel->owner().updateNotifySettings(
_channel,
std::nullopt,
_checked);
}
QString SilentToggle::tooltipText() const {
return _checked
? tr::lng_wont_be_notified(tr::now)
: tr::lng_will_be_notified(tr::now);
}
QPoint SilentToggle::tooltipPos() const {
return QCursor::pos();
}
bool SilentToggle::tooltipWindowActive() const {
return Ui::AppInFocus() && InFocusChain(window());
}
QPoint SilentToggle::prepareRippleStartPosition() const {
const auto result = mapFromGlobal(QCursor::pos())
- _st.rippleAreaPosition;
const auto rect = QRect(0, 0, _st.rippleAreaSize, _st.rippleAreaSize);
return rect.contains(result)
? result
: DisabledRippleStartPosition();
}
QImage SilentToggle::prepareRippleMask() const {
return RippleAnimation::ellipseMask(
QSize(_st.rippleAreaSize, _st.rippleAreaSize));
}
} // namespace Ui