1097 lines
31 KiB
C++
1097 lines
31 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/media/history_view_photo.h"
|
|
|
|
#include "history/history_item_components.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history.h"
|
|
#include "history/view/history_view_element.h"
|
|
#include "history/view/history_view_cursor_state.h"
|
|
#include "history/view/media/history_view_media_common.h"
|
|
#include "history/view/media/history_view_media_spoiler.h"
|
|
#include "media/streaming/media_streaming_instance.h"
|
|
#include "media/streaming/media_streaming_player.h"
|
|
#include "media/streaming/media_streaming_document.h"
|
|
#include "media/streaming/media_streaming_utility.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_session_settings.h"
|
|
#include "ui/image/image.h"
|
|
#include "ui/effects/spoiler_mess.h"
|
|
#include "ui/chat/chat_style.h"
|
|
#include "ui/grouped_layout.h"
|
|
#include "ui/cached_round_corners.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/power_saving.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_stories.h"
|
|
#include "data/data_streaming.h"
|
|
#include "data/data_photo.h"
|
|
#include "data/data_photo_media.h"
|
|
#include "data/data_file_click_handler.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_auto_download.h"
|
|
#include "core/application.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace HistoryView {
|
|
namespace {
|
|
|
|
constexpr auto kStoryWidth = 720;
|
|
constexpr auto kStoryHeight = 1280;
|
|
|
|
using Data::PhotoSize;
|
|
|
|
} // namespace
|
|
|
|
struct Photo::Streamed {
|
|
explicit Streamed(std::shared_ptr<::Media::Streaming::Document> shared);
|
|
::Media::Streaming::Instance instance;
|
|
::Media::Streaming::FrameRequest frozenRequest;
|
|
QImage frozenFrame;
|
|
std::array<QImage, 4> roundingCorners;
|
|
QImage roundingMask;
|
|
};
|
|
|
|
Photo::Streamed::Streamed(
|
|
std::shared_ptr<::Media::Streaming::Document> shared)
|
|
: instance(std::move(shared), nullptr) {
|
|
}
|
|
|
|
Photo::Photo(
|
|
not_null<Element*> parent,
|
|
not_null<HistoryItem*> realParent,
|
|
not_null<PhotoData*> photo,
|
|
bool spoiler)
|
|
: File(parent, realParent)
|
|
, _data(photo)
|
|
, _storyId(realParent->media()
|
|
? realParent->media()->storyId()
|
|
: FullStoryId())
|
|
, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
|
|
, _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr) {
|
|
_caption = createCaption(realParent);
|
|
create(realParent->fullId());
|
|
}
|
|
|
|
Photo::Photo(
|
|
not_null<Element*> parent,
|
|
not_null<PeerData*> chat,
|
|
not_null<PhotoData*> photo,
|
|
int width)
|
|
: File(parent, parent->data())
|
|
, _data(photo)
|
|
, _serviceWidth(width) {
|
|
create(parent->data()->fullId(), chat);
|
|
}
|
|
|
|
Photo::~Photo() {
|
|
if (_streamed || _dataMedia) {
|
|
if (_streamed) {
|
|
_data->owner().streaming().keepAlive(_data);
|
|
stopAnimation();
|
|
}
|
|
if (_dataMedia) {
|
|
_data->owner().keepAlive(base::take(_dataMedia));
|
|
_parent->checkHeavyPart();
|
|
}
|
|
}
|
|
togglePollingStory(false);
|
|
}
|
|
|
|
void Photo::create(FullMsgId contextId, PeerData *chat) {
|
|
setLinks(
|
|
std::make_shared<PhotoOpenClickHandler>(
|
|
_data,
|
|
crl::guard(this, [=](FullMsgId id) { showPhoto(id); }),
|
|
contextId),
|
|
std::make_shared<PhotoSaveClickHandler>(_data, contextId, chat),
|
|
std::make_shared<PhotoCancelClickHandler>(
|
|
_data,
|
|
crl::guard(this, [=](FullMsgId id) {
|
|
_parent->delegate()->elementCancelUpload(id);
|
|
}),
|
|
contextId));
|
|
if ((_dataMedia = _data->activeMediaView())) {
|
|
dataMediaCreated();
|
|
} else if (_data->inlineThumbnailBytes().isEmpty()
|
|
&& (_data->hasExact(PhotoSize::Small)
|
|
|| _data->hasExact(PhotoSize::Thumbnail))) {
|
|
_data->load(PhotoSize::Small, contextId);
|
|
}
|
|
if (_spoiler) {
|
|
createSpoilerLink(_spoiler.get());
|
|
}
|
|
}
|
|
|
|
void Photo::ensureDataMediaCreated() const {
|
|
if (_dataMedia) {
|
|
return;
|
|
}
|
|
_dataMedia = _data->createMediaView();
|
|
dataMediaCreated();
|
|
}
|
|
|
|
void Photo::dataMediaCreated() const {
|
|
Expects(_dataMedia != nullptr);
|
|
|
|
if (_data->inlineThumbnailBytes().isEmpty()
|
|
&& !_dataMedia->image(PhotoSize::Large)
|
|
&& !_dataMedia->image(PhotoSize::Thumbnail)) {
|
|
_dataMedia->wanted(PhotoSize::Small, _realParent->fullId());
|
|
}
|
|
history()->owner().registerHeavyViewPart(_parent);
|
|
togglePollingStory(true);
|
|
}
|
|
|
|
bool Photo::hasHeavyPart() const {
|
|
return (_spoiler && _spoiler->animation) || _streamed || _dataMedia;
|
|
}
|
|
|
|
void Photo::unloadHeavyPart() {
|
|
stopAnimation();
|
|
_dataMedia = nullptr;
|
|
if (_spoiler) {
|
|
_spoiler->background = _spoiler->cornerCache = QImage();
|
|
_spoiler->animation = nullptr;
|
|
}
|
|
_imageCache = QImage();
|
|
_caption.unloadPersistentAnimation();
|
|
togglePollingStory(false);
|
|
}
|
|
|
|
void Photo::togglePollingStory(bool enabled) const {
|
|
const auto pollingStory = (enabled ? 1 : 0);
|
|
if (!_storyId || _pollingStory == pollingStory) {
|
|
return;
|
|
}
|
|
const auto polling = Data::Stories::Polling::Chat;
|
|
if (!enabled) {
|
|
_data->owner().stories().unregisterPolling(_storyId, polling);
|
|
} else if (
|
|
!_data->owner().stories().registerPolling(_storyId, polling)) {
|
|
return;
|
|
}
|
|
_pollingStory = pollingStory;
|
|
}
|
|
|
|
QSize Photo::countOptimalSize() {
|
|
if (_serviceWidth > 0) {
|
|
return { int(_serviceWidth), int(_serviceWidth) };
|
|
}
|
|
|
|
if (_parent->media() != this) {
|
|
_caption = Ui::Text::String();
|
|
} else if (_caption.hasSkipBlock()) {
|
|
_caption.updateSkipBlock(
|
|
_parent->skipBlockWidth(),
|
|
_parent->skipBlockHeight());
|
|
}
|
|
|
|
const auto dimensions = photoSize();
|
|
const auto scaled = CountDesiredMediaSize(dimensions);
|
|
const auto minWidth = std::clamp(
|
|
_parent->minWidthForMedia(),
|
|
(_parent->hasBubble()
|
|
? st::historyPhotoBubbleMinWidth
|
|
: st::minPhotoSize),
|
|
st::maxMediaSize);
|
|
const auto maxActualWidth = qMax(scaled.width(), minWidth);
|
|
auto maxWidth = qMax(maxActualWidth, scaled.height());
|
|
auto minHeight = qMax(scaled.height(), st::minPhotoSize);
|
|
if (_parent->hasBubble() && !_caption.isEmpty()) {
|
|
maxWidth = qMax(
|
|
maxWidth,
|
|
(st::msgPadding.left()
|
|
+ _caption.maxWidth()
|
|
+ st::msgPadding.right()));
|
|
minHeight = adjustHeightForLessCrop(
|
|
dimensions,
|
|
{ maxWidth, minHeight });
|
|
if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
|
|
accumulate_max(maxWidth, botTop->maxWidth);
|
|
minHeight += botTop->height;
|
|
}
|
|
minHeight += st::mediaCaptionSkip + _caption.minHeight();
|
|
if (isBubbleBottom()) {
|
|
minHeight += st::msgPadding.bottom();
|
|
}
|
|
}
|
|
return { maxWidth, minHeight };
|
|
}
|
|
|
|
QSize Photo::countCurrentSize(int newWidth) {
|
|
if (_serviceWidth) {
|
|
return { int(_serviceWidth), int(_serviceWidth) };
|
|
}
|
|
const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize);
|
|
const auto minWidth = std::clamp(
|
|
_parent->minWidthForMedia(),
|
|
(_parent->hasBubble()
|
|
? st::historyPhotoBubbleMinWidth
|
|
: st::minPhotoSize),
|
|
thumbMaxWidth);
|
|
const auto dimensions = photoSize();
|
|
auto pix = CountPhotoMediaSize(
|
|
CountDesiredMediaSize(dimensions),
|
|
newWidth,
|
|
maxWidth());
|
|
newWidth = qMax(pix.width(), minWidth);
|
|
auto newHeight = qMax(pix.height(), st::minPhotoSize);
|
|
if (_parent->hasBubble() && !_caption.isEmpty()) {
|
|
auto captionMaxWidth = st::msgPadding.left()
|
|
+ _caption.maxWidth()
|
|
+ st::msgPadding.right();
|
|
const auto botTop = _parent->Get<FakeBotAboutTop>();
|
|
if (botTop) {
|
|
accumulate_max(captionMaxWidth, botTop->maxWidth);
|
|
}
|
|
const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
|
|
newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
|
|
newHeight = adjustHeightForLessCrop(
|
|
dimensions,
|
|
{ newWidth, newHeight });
|
|
const auto captionw = newWidth
|
|
- st::msgPadding.left()
|
|
- st::msgPadding.right();
|
|
if (botTop) {
|
|
newHeight += botTop->height;
|
|
}
|
|
newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
|
|
if (isBubbleBottom()) {
|
|
newHeight += st::msgPadding.bottom();
|
|
}
|
|
}
|
|
return { newWidth, newHeight };
|
|
}
|
|
|
|
int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const {
|
|
if (dimensions.isEmpty()
|
|
|| !::Media::Streaming::FrameResizeMayExpand(current, dimensions)) {
|
|
return current.height();
|
|
}
|
|
return qMax(
|
|
current.height(),
|
|
current.width() * dimensions.height() / dimensions.width());
|
|
}
|
|
|
|
void Photo::draw(Painter &p, const PaintContext &context) const {
|
|
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
|
|
return;
|
|
} else if (_storyId && _data->isNull()) {
|
|
return;
|
|
}
|
|
|
|
ensureDataMediaCreated();
|
|
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
|
|
const auto st = context.st;
|
|
const auto sti = context.imageStyle();
|
|
const auto stm = context.messageStyle();
|
|
auto loaded = _dataMedia->loaded();
|
|
auto displayLoading = _data->displayLoading();
|
|
|
|
auto inWebPage = (_parent->media() != this);
|
|
auto paintx = 0, painty = 0, paintw = width(), painth = height();
|
|
auto bubble = _parent->hasBubble();
|
|
|
|
auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right();
|
|
|
|
if (displayLoading) {
|
|
ensureAnimation();
|
|
if (!_animation->radial.animating()) {
|
|
_animation->radial.start(_dataMedia->progress());
|
|
}
|
|
}
|
|
const auto radial = isRadialAnimation();
|
|
const auto botTop = _parent->Get<FakeBotAboutTop>();
|
|
|
|
auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
|
|
if (_serviceWidth > 0) {
|
|
paintUserpicFrame(p, context, rthumb.topLeft());
|
|
} else {
|
|
const auto rounding = inWebPage
|
|
? std::optional<Ui::BubbleRounding>()
|
|
: adjustedBubbleRoundingWithCaption(_caption);
|
|
if (bubble) {
|
|
if (!_caption.isEmpty()) {
|
|
painth -= st::mediaCaptionSkip + _caption.countHeight(captionw);
|
|
if (botTop) {
|
|
painth -= botTop->height;
|
|
}
|
|
if (isBubbleBottom()) {
|
|
painth -= st::msgPadding.bottom();
|
|
}
|
|
rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
|
|
}
|
|
} else {
|
|
Assert(rounding.has_value());
|
|
fillImageShadow(p, rthumb, *rounding, context);
|
|
}
|
|
const auto revealed = _spoiler
|
|
? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
|
|
: 1.;
|
|
if (revealed < 1.) {
|
|
validateSpoilerImageCache(rthumb.size(), rounding);
|
|
}
|
|
if (revealed > 0.) {
|
|
validateImageCache(rthumb.size(), rounding);
|
|
p.drawImage(rthumb.topLeft(), _imageCache);
|
|
}
|
|
if (revealed < 1.) {
|
|
p.setOpacity(1. - revealed);
|
|
p.drawImage(rthumb.topLeft(), _spoiler->background);
|
|
fillImageSpoiler(p, _spoiler.get(), rthumb, context);
|
|
p.setOpacity(1.);
|
|
}
|
|
if (context.selected()) {
|
|
fillImageOverlay(p, rthumb, rounding, context);
|
|
}
|
|
}
|
|
if (radial || (!loaded && !_data->loading())) {
|
|
const auto radialOpacity = (radial && loaded && !_data->uploading())
|
|
? _animation->radial.opacity() :
|
|
1.;
|
|
const auto innerSize = st::msgFileLayout.thumbSize;
|
|
QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
|
|
p.setPen(Qt::NoPen);
|
|
if (context.selected()) {
|
|
p.setBrush(st->msgDateImgBgSelected());
|
|
} else if (isThumbAnimation()) {
|
|
const auto over = _animation->a_thumbOver.value(1.);
|
|
p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
|
|
} else {
|
|
const auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
|
|
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
|
|
}
|
|
|
|
p.setOpacity(radialOpacity * p.opacity());
|
|
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawEllipse(inner);
|
|
}
|
|
|
|
p.setOpacity(radialOpacity);
|
|
const auto &icon = (radial || _data->loading())
|
|
? sti->historyFileThumbCancel
|
|
: sti->historyFileThumbDownload;
|
|
icon.paintInCenter(p, inner);
|
|
p.setOpacity(1);
|
|
if (radial) {
|
|
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
|
|
_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
|
|
}
|
|
}
|
|
|
|
// date
|
|
if (!_caption.isEmpty()) {
|
|
p.setPen(stm->historyTextFg);
|
|
_parent->prepareCustomEmojiPaint(p, context, _caption);
|
|
auto top = painty + painth + st::mediaCaptionSkip;
|
|
if (botTop) {
|
|
botTop->text.drawLeftElided(
|
|
p,
|
|
st::msgPadding.left(),
|
|
top,
|
|
captionw,
|
|
_parent->width());
|
|
top += botTop->height;
|
|
}
|
|
_caption.draw(p, {
|
|
.position = QPoint(st::msgPadding.left(), top),
|
|
.availableWidth = captionw,
|
|
.palette = &stm->textPalette,
|
|
.pre = stm->preCache.get(),
|
|
.blockquote = stm->blockquoteCache.get(),
|
|
.colors = context.st->highlightColors(),
|
|
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
|
.now = context.now,
|
|
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
|
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
|
.selection = context.selection,
|
|
});
|
|
} else if (!inWebPage) {
|
|
auto fullRight = paintx + paintw;
|
|
auto fullBottom = painty + painth;
|
|
if (needInfoDisplay()) {
|
|
_parent->drawInfo(
|
|
p,
|
|
context,
|
|
fullRight,
|
|
fullBottom,
|
|
2 * paintx + paintw,
|
|
InfoDisplayType::Image);
|
|
}
|
|
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
|
|
auto fastShareLeft = (fullRight + st::historyFastShareLeft);
|
|
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
|
|
_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Photo::validateUserpicImageCache(QSize size, bool forum) const {
|
|
const auto forumValue = forum ? 1 : 0;
|
|
const auto large = _dataMedia->image(PhotoSize::Large);
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto blurredValue = large ? 0 : 1;
|
|
if (_imageCache.size() == (size * ratio)
|
|
&& _imageCacheForum == forumValue
|
|
&& _imageCacheBlurred == blurredValue) {
|
|
return;
|
|
}
|
|
auto original = [&] {
|
|
if (large) {
|
|
return large->original();
|
|
} else if (const auto thumbnail = _dataMedia->image(
|
|
PhotoSize::Thumbnail)) {
|
|
return thumbnail->original();
|
|
} else if (const auto small = _dataMedia->image(
|
|
PhotoSize::Small)) {
|
|
return small->original();
|
|
} else if (const auto blurred = _dataMedia->thumbnailInline()) {
|
|
return blurred->original();
|
|
} else {
|
|
return Image::Empty()->original();
|
|
}
|
|
}();
|
|
auto args = Images::PrepareArgs();
|
|
if (blurredValue) {
|
|
args = args.blurred();
|
|
}
|
|
original = Images::Prepare(std::move(original), size * ratio, args);
|
|
if (forumValue) {
|
|
original = Images::Round(
|
|
std::move(original),
|
|
Images::CornersMask(std::min(size.width(), size.height())
|
|
* Ui::ForumUserpicRadiusMultiplier()));
|
|
} else {
|
|
original = Images::Circle(std::move(original));
|
|
}
|
|
_imageCache = std::move(original);
|
|
_imageCacheForum = forumValue;
|
|
_imageCacheBlurred = blurredValue;
|
|
}
|
|
|
|
void Photo::validateImageCache(
|
|
QSize outer,
|
|
std::optional<Ui::BubbleRounding> rounding) const {
|
|
const auto large = _dataMedia->image(PhotoSize::Large);
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto blurredValue = large ? 0 : 1;
|
|
if (_imageCache.size() == (outer * ratio)
|
|
&& _imageCacheRounding == rounding
|
|
&& _imageCacheBlurred == blurredValue) {
|
|
return;
|
|
}
|
|
_imageCache = Images::Round(
|
|
prepareImageCache(outer),
|
|
MediaRoundingMask(rounding));
|
|
_imageCacheRounding = rounding;
|
|
_imageCacheBlurred = blurredValue;
|
|
}
|
|
|
|
void Photo::validateSpoilerImageCache(
|
|
QSize outer,
|
|
std::optional<Ui::BubbleRounding> rounding) const {
|
|
Expects(_spoiler != nullptr);
|
|
|
|
const auto ratio = style::DevicePixelRatio();
|
|
if (_spoiler->background.size() == (outer * ratio)
|
|
&& _spoiler->backgroundRounding == rounding) {
|
|
return;
|
|
}
|
|
_spoiler->background = Images::Round(
|
|
prepareImageCacheWithLarge(outer, nullptr),
|
|
MediaRoundingMask(rounding));
|
|
_spoiler->backgroundRounding = rounding;
|
|
}
|
|
|
|
QImage Photo::prepareImageCache(QSize outer) const {
|
|
return prepareImageCacheWithLarge(
|
|
outer,
|
|
_dataMedia->image(PhotoSize::Large));
|
|
}
|
|
|
|
QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {
|
|
using Size = PhotoSize;
|
|
auto blurred = (Image*)nullptr;
|
|
if (const auto embedded = _dataMedia->thumbnailInline()) {
|
|
blurred = embedded;
|
|
} else if (const auto thumbnail = _dataMedia->image(Size::Thumbnail)) {
|
|
blurred = thumbnail;
|
|
} else if (const auto small = _dataMedia->image(Size::Small)) {
|
|
blurred = small;
|
|
} else {
|
|
blurred = large;
|
|
}
|
|
const auto resize = large
|
|
? ::Media::Streaming::DecideFrameResize(outer, large->size())
|
|
: ::Media::Streaming::ExpandDecision();
|
|
return PrepareWithBlurredBackground(outer, resize, large, blurred);
|
|
}
|
|
|
|
void Photo::paintUserpicFrame(
|
|
Painter &p,
|
|
QPoint photoPosition,
|
|
bool markFrameShown) const {
|
|
const auto autoplay = _data->videoCanBePlayed()
|
|
&& videoAutoplayEnabled();
|
|
const auto startPlay = autoplay && !_streamed;
|
|
if (startPlay) {
|
|
const_cast<Photo*>(this)->playAnimation(true);
|
|
} else {
|
|
checkStreamedIsStarted();
|
|
}
|
|
|
|
const auto size = QSize(width(), height());
|
|
const auto rect = QRect(photoPosition, size);
|
|
const auto forum = _parent->data()->history()->isForum();
|
|
|
|
if (_streamed
|
|
&& _streamed->instance.player().ready()
|
|
&& !_streamed->instance.player().videoSize().isEmpty()) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
auto request = ::Media::Streaming::FrameRequest();
|
|
request.outer = request.resize = size * ratio;
|
|
if (forum) {
|
|
const auto radius = int(std::min(size.width(), size.height())
|
|
* Ui::ForumUserpicRadiusMultiplier());
|
|
if (_streamed->roundingCorners[0].width() != radius * ratio) {
|
|
_streamed->roundingCorners = Images::CornersMask(radius);
|
|
}
|
|
request.rounding = Images::CornersMaskRef(
|
|
_streamed->roundingCorners);
|
|
} else {
|
|
if (_streamed->roundingMask.size() != request.outer) {
|
|
_streamed->roundingMask = Images::EllipseMask(size);
|
|
}
|
|
request.mask = _streamed->roundingMask;
|
|
}
|
|
if (_streamed->instance.playerLocked()) {
|
|
if (_streamed->frozenFrame.isNull()
|
|
|| _streamed->frozenRequest != request) {
|
|
_streamed->frozenRequest = request;
|
|
_streamed->frozenFrame = _streamed->instance.frame(request);
|
|
}
|
|
p.drawImage(rect, _streamed->frozenFrame);
|
|
} else {
|
|
_streamed->frozenFrame = QImage();
|
|
p.drawImage(rect, _streamed->instance.frame(request));
|
|
if (markFrameShown) {
|
|
_streamed->instance.markFrameShown();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
validateUserpicImageCache(size, forum);
|
|
p.drawImage(rect, _imageCache);
|
|
}
|
|
|
|
void Photo::paintUserpicFrame(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
QPoint photoPosition) const {
|
|
paintUserpicFrame(p, photoPosition, !context.paused);
|
|
|
|
if (_data->videoCanBePlayed() && !_streamed) {
|
|
const auto st = context.st;
|
|
const auto sti = context.imageStyle();
|
|
const auto innerSize = st::msgFileLayout.thumbSize;
|
|
auto inner = QRect(
|
|
photoPosition.x() + (width() - innerSize) / 2,
|
|
photoPosition.y() + (height() - innerSize) / 2,
|
|
innerSize,
|
|
innerSize);
|
|
p.setPen(Qt::NoPen);
|
|
if (context.selected()) {
|
|
p.setBrush(st->msgDateImgBgSelected());
|
|
} else {
|
|
const auto over = ClickHandler::showAsActive(_openl);
|
|
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
|
|
}
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawEllipse(inner);
|
|
}
|
|
sti->historyFileThumbPlay.paintInCenter(p, inner);
|
|
}
|
|
}
|
|
|
|
QSize Photo::photoSize() const {
|
|
if (_storyId) {
|
|
return { kStoryWidth, kStoryHeight };
|
|
}
|
|
return QSize(_data->width(), _data->height());
|
|
}
|
|
|
|
TextState Photo::textState(QPoint point, StateRequest request) const {
|
|
auto result = TextState(_parent);
|
|
|
|
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
|
|
return result;
|
|
} else if (_storyId && _data->isNull()) {
|
|
return result;
|
|
}
|
|
auto paintx = 0, painty = 0, paintw = width(), painth = height();
|
|
auto bubble = _parent->hasBubble();
|
|
|
|
if (bubble && !_caption.isEmpty()) {
|
|
const auto captionw = paintw
|
|
- st::msgPadding.left()
|
|
- st::msgPadding.right();
|
|
painth -= _caption.countHeight(captionw);
|
|
if (isBubbleBottom()) {
|
|
painth -= st::msgPadding.bottom();
|
|
}
|
|
if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) {
|
|
result = TextState(_parent, _caption.getState(
|
|
point - QPoint(st::msgPadding.left(), painth),
|
|
captionw,
|
|
request.forText()));
|
|
return result;
|
|
}
|
|
if (const auto botTop = _parent->Get<FakeBotAboutTop>()) {
|
|
painth -= botTop->height;
|
|
}
|
|
painth -= st::mediaCaptionSkip;
|
|
}
|
|
if (QRect(paintx, painty, paintw, painth).contains(point)) {
|
|
ensureDataMediaCreated();
|
|
result.link = (_spoiler && !_spoiler->revealed)
|
|
? _spoiler->link
|
|
: _data->uploading()
|
|
? _cancell
|
|
: _dataMedia->loaded()
|
|
? _openl
|
|
: _data->loading()
|
|
? _cancell
|
|
: _savel;
|
|
}
|
|
if (_caption.isEmpty() && _parent->media() == this) {
|
|
auto fullRight = paintx + paintw;
|
|
auto fullBottom = painty + painth;
|
|
const auto bottomInfoResult = _parent->bottomInfoTextState(
|
|
fullRight,
|
|
fullBottom,
|
|
point,
|
|
InfoDisplayType::Image);
|
|
if (bottomInfoResult.link
|
|
|| bottomInfoResult.cursor != CursorState::None
|
|
|| bottomInfoResult.customTooltip) {
|
|
return bottomInfoResult;
|
|
}
|
|
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
|
|
auto fastShareLeft = (fullRight + st::historyFastShareLeft);
|
|
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
|
|
if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
|
|
result.link = _parent->rightActionLink(point
|
|
- QPoint(fastShareLeft, fastShareTop));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QSize Photo::sizeForGroupingOptimal(int maxWidth) const {
|
|
const auto size = photoSize();
|
|
return { std::max(size.width(), 1), std::max(size.height(), 1)};
|
|
}
|
|
|
|
QSize Photo::sizeForGrouping(int width) const {
|
|
return sizeForGroupingOptimal(width);
|
|
}
|
|
|
|
void Photo::drawGrouped(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
const QRect &geometry,
|
|
RectParts sides,
|
|
Ui::BubbleRounding rounding,
|
|
float64 highlightOpacity,
|
|
not_null<uint64*> cacheKey,
|
|
not_null<QPixmap*> cache) const {
|
|
ensureDataMediaCreated();
|
|
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
|
|
|
|
const auto st = context.st;
|
|
const auto sti = context.imageStyle();
|
|
const auto loaded = _dataMedia->loaded();
|
|
const auto displayLoading = _data->displayLoading();
|
|
|
|
if (displayLoading) {
|
|
ensureAnimation();
|
|
if (!_animation->radial.animating()) {
|
|
_animation->radial.start(_dataMedia->progress());
|
|
}
|
|
}
|
|
const auto radial = isRadialAnimation();
|
|
|
|
const auto revealed = _spoiler
|
|
? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
|
|
: 1.;
|
|
if (revealed < 1.) {
|
|
validateSpoilerImageCache(geometry.size(), rounding);
|
|
}
|
|
if (revealed > 0.) {
|
|
validateGroupedCache(geometry, rounding, cacheKey, cache);
|
|
p.drawPixmap(geometry.topLeft(), *cache);
|
|
}
|
|
if (revealed < 1.) {
|
|
p.setOpacity(1. - revealed);
|
|
p.drawImage(geometry.topLeft(), _spoiler->background);
|
|
fillImageSpoiler(p, _spoiler.get(), geometry, context);
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
const auto overlayOpacity = context.selected()
|
|
? (1. - highlightOpacity)
|
|
: highlightOpacity;
|
|
if (overlayOpacity > 0.) {
|
|
p.setOpacity(overlayOpacity);
|
|
fillImageOverlay(p, geometry, rounding, context);
|
|
if (!context.selected()) {
|
|
fillImageOverlay(p, geometry, rounding, context);
|
|
}
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
const auto displayState = radial
|
|
|| (!loaded && !_data->loading())
|
|
|| _data->waitingForAlbum();
|
|
if (displayState) {
|
|
const auto radialOpacity = radial
|
|
? _animation->radial.opacity()
|
|
: 1.;
|
|
const auto backOpacity = (loaded && !_data->uploading())
|
|
? radialOpacity
|
|
: 1.;
|
|
const auto radialSize = st::historyGroupRadialSize;
|
|
const auto inner = QRect(
|
|
geometry.x() + (geometry.width() - radialSize) / 2,
|
|
geometry.y() + (geometry.height() - radialSize) / 2,
|
|
radialSize,
|
|
radialSize);
|
|
p.setPen(Qt::NoPen);
|
|
if (context.selected()) {
|
|
p.setBrush(st->msgDateImgBgSelected());
|
|
} else if (isThumbAnimation()) {
|
|
auto over = _animation->a_thumbOver.value(1.);
|
|
p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
|
|
} else {
|
|
auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
|
|
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
|
|
}
|
|
|
|
p.setOpacity(backOpacity * p.opacity());
|
|
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawEllipse(inner);
|
|
}
|
|
|
|
const auto &icon = _data->waitingForAlbum()
|
|
? sti->historyFileThumbWaiting
|
|
: (radial || _data->loading())
|
|
? sti->historyFileThumbCancel
|
|
: sti->historyFileThumbDownload;
|
|
const auto previous = _data->waitingForAlbum()
|
|
? &sti->historyFileThumbCancel
|
|
: nullptr;
|
|
p.setOpacity(backOpacity);
|
|
if (previous && radialOpacity > 0. && radialOpacity < 1.) {
|
|
PaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner);
|
|
} else {
|
|
icon.paintInCenter(p, inner);
|
|
}
|
|
p.setOpacity(1);
|
|
if (radial) {
|
|
const auto line = st::historyGroupRadialLine;
|
|
const auto rinner = inner.marginsRemoved({ line, line, line, line });
|
|
_animation->radial.draw(p, rinner, line, sti->historyFileThumbRadialFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
TextState Photo::getStateGrouped(
|
|
const QRect &geometry,
|
|
RectParts sides,
|
|
QPoint point,
|
|
StateRequest request) const {
|
|
if (!geometry.contains(point)) {
|
|
return {};
|
|
}
|
|
ensureDataMediaCreated();
|
|
return TextState(_parent, (_spoiler && !_spoiler->revealed)
|
|
? _spoiler->link
|
|
: _data->uploading()
|
|
? _cancell
|
|
: _dataMedia->loaded()
|
|
? _openl
|
|
: _data->loading()
|
|
? _cancell
|
|
: _savel);
|
|
}
|
|
|
|
float64 Photo::dataProgress() const {
|
|
ensureDataMediaCreated();
|
|
return _dataMedia->progress();
|
|
}
|
|
|
|
bool Photo::dataFinished() const {
|
|
return !_data->loading()
|
|
&& (!_data->uploading() || _data->waitingForAlbum());
|
|
}
|
|
|
|
bool Photo::dataLoaded() const {
|
|
ensureDataMediaCreated();
|
|
return _dataMedia->loaded();
|
|
}
|
|
|
|
bool Photo::needInfoDisplay() const {
|
|
if (_parent->data()->isFakeBotAbout()) {
|
|
return false;
|
|
}
|
|
return _parent->data()->isSending()
|
|
|| _parent->data()->hasFailed()
|
|
|| _parent->isUnderCursor()
|
|
|| _parent->isLastAndSelfMessage();
|
|
}
|
|
|
|
void Photo::validateGroupedCache(
|
|
const QRect &geometry,
|
|
Ui::BubbleRounding rounding,
|
|
not_null<uint64*> cacheKey,
|
|
not_null<QPixmap*> cache) const {
|
|
using Option = Images::Option;
|
|
|
|
ensureDataMediaCreated();
|
|
|
|
const auto loaded = _dataMedia->loaded();
|
|
const auto loadLevel = loaded
|
|
? 2
|
|
: (_dataMedia->thumbnailInline()
|
|
|| _dataMedia->image(PhotoSize::Small)
|
|
|| _dataMedia->image(PhotoSize::Thumbnail))
|
|
? 1
|
|
: 0;
|
|
const auto width = geometry.width();
|
|
const auto height = geometry.height();
|
|
const auto options = (loaded ? Option() : Option::Blur);
|
|
const auto key = (uint64(width) << 48)
|
|
| (uint64(height) << 32)
|
|
| (uint64(options) << 16)
|
|
| (uint64(rounding.key()) << 8)
|
|
| (uint64(loadLevel));
|
|
if (*cacheKey == key) {
|
|
return;
|
|
}
|
|
|
|
const auto unscaled = photoSize();
|
|
const auto originalWidth = style::ConvertScale(unscaled.width());
|
|
const auto originalHeight = style::ConvertScale(unscaled.height());
|
|
const auto pixSize = Ui::GetImageScaleSizeForGeometry(
|
|
{ originalWidth, originalHeight },
|
|
{ width, height });
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto image = _dataMedia->image(PhotoSize::Large)
|
|
? _dataMedia->image(PhotoSize::Large)
|
|
: _dataMedia->image(PhotoSize::Thumbnail)
|
|
? _dataMedia->image(PhotoSize::Thumbnail)
|
|
: _dataMedia->image(PhotoSize::Small)
|
|
? _dataMedia->image(PhotoSize::Small)
|
|
: _dataMedia->thumbnailInline()
|
|
? _dataMedia->thumbnailInline()
|
|
: Image::BlankMedia().get();
|
|
|
|
*cacheKey = key;
|
|
auto scaled = Images::Prepare(
|
|
image->original(),
|
|
pixSize * ratio,
|
|
{ .options = options, .outer = { width, height } });
|
|
auto rounded = Images::Round(
|
|
std::move(scaled),
|
|
MediaRoundingMask(rounding));
|
|
*cache = Ui::PixmapFromImage(std::move(rounded));
|
|
}
|
|
|
|
bool Photo::createStreamingObjects() {
|
|
using namespace ::Media::Streaming;
|
|
|
|
setStreamed(std::make_unique<Streamed>(
|
|
history()->owner().streaming().sharedDocument(
|
|
_data,
|
|
_realParent->fullId())));
|
|
_streamed->instance.player().updates(
|
|
) | rpl::start_with_next_error([=](Update &&update) {
|
|
handleStreamingUpdate(std::move(update));
|
|
}, [=](Error &&error) {
|
|
handleStreamingError(std::move(error));
|
|
}, _streamed->instance.lifetime());
|
|
if (_streamed->instance.ready()) {
|
|
streamingReady(base::duplicate(_streamed->instance.info()));
|
|
}
|
|
if (!_streamed->instance.valid()) {
|
|
stopAnimation();
|
|
return false;
|
|
}
|
|
checkStreamedIsStarted();
|
|
return true;
|
|
}
|
|
|
|
void Photo::setStreamed(std::unique_ptr<Streamed> value) {
|
|
const auto removed = (_streamed && !value);
|
|
const auto set = (!_streamed && value);
|
|
_streamed = std::move(value);
|
|
if (set) {
|
|
history()->owner().registerHeavyViewPart(_parent);
|
|
togglePollingStory(true);
|
|
} else if (removed) {
|
|
_parent->checkHeavyPart();
|
|
}
|
|
}
|
|
|
|
void Photo::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) {
|
|
repaintStreamedContent();
|
|
}, [&](const PreloadedAudio &update) {
|
|
}, [&](const UpdateAudio &update) {
|
|
}, [&](const WaitingForData &update) {
|
|
}, [&](MutedByOther) {
|
|
}, [&](Finished) {
|
|
});
|
|
}
|
|
|
|
void Photo::handleStreamingError(::Media::Streaming::Error &&error) {
|
|
_data->setVideoPlaybackFailed();
|
|
stopAnimation();
|
|
}
|
|
|
|
void Photo::repaintStreamedContent() {
|
|
if (_streamed && !_streamed->frozenFrame.isNull()) {
|
|
return;
|
|
} else if (_parent->delegate()->elementAnimationsPaused()) {
|
|
return;
|
|
}
|
|
repaint();
|
|
}
|
|
|
|
void Photo::streamingReady(::Media::Streaming::Information &&info) {
|
|
repaint();
|
|
}
|
|
|
|
void Photo::checkAnimation() {
|
|
if (_streamed && !videoAutoplayEnabled()) {
|
|
stopAnimation();
|
|
}
|
|
}
|
|
|
|
void Photo::stopAnimation() {
|
|
setStreamed(nullptr);
|
|
}
|
|
|
|
void Photo::playAnimation(bool autoplay) {
|
|
ensureDataMediaCreated();
|
|
if (_streamed && autoplay) {
|
|
return;
|
|
} else if (_streamed && videoAutoplayEnabled()) {
|
|
showPhoto(_parent->data()->fullId());
|
|
return;
|
|
}
|
|
if (_streamed) {
|
|
stopAnimation();
|
|
} else if (_data->videoCanBePlayed()) {
|
|
if (!videoAutoplayEnabled()) {
|
|
history()->owner().checkPlayingAnimations();
|
|
}
|
|
if (!createStreamingObjects()) {
|
|
_data->setVideoPlaybackFailed();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Photo::checkStreamedIsStarted() const {
|
|
if (!_streamed) {
|
|
return;
|
|
} else if (_streamed->instance.paused()) {
|
|
_streamed->instance.resume();
|
|
}
|
|
if (_streamed
|
|
&& !_streamed->instance.active()
|
|
&& !_streamed->instance.failed()) {
|
|
const auto position = _data->videoStartPosition();
|
|
auto options = ::Media::Streaming::PlaybackOptions();
|
|
options.position = position;
|
|
options.mode = ::Media::Streaming::Mode::Video;
|
|
options.loop = true;
|
|
_streamed->instance.play(options);
|
|
}
|
|
}
|
|
|
|
bool Photo::videoAutoplayEnabled() const {
|
|
return Data::AutoDownload::ShouldAutoPlay(
|
|
_data->session().settings().autoDownload(),
|
|
_realParent->history()->peer,
|
|
_data);
|
|
}
|
|
|
|
TextForMimeData Photo::selectedText(TextSelection selection) const {
|
|
return _caption.toTextForMimeData(selection);
|
|
}
|
|
|
|
void Photo::hideSpoilers() {
|
|
_caption.setSpoilerRevealed(false, anim::type::instant);
|
|
if (_spoiler) {
|
|
_spoiler->revealed = false;
|
|
}
|
|
}
|
|
|
|
bool Photo::needsBubble() const {
|
|
if (_storyId || !_caption.isEmpty()) {
|
|
return true;
|
|
}
|
|
const auto item = _parent->data();
|
|
return !item->isService()
|
|
&& (item->repliesAreComments()
|
|
|| item->externalReply()
|
|
|| item->viaBot()
|
|
|| _parent->displayedReply()
|
|
|| _parent->displayForwardedFrom()
|
|
|| _parent->displayFromName()
|
|
|| _parent->displayedTopicButton());
|
|
}
|
|
|
|
QPoint Photo::resolveCustomInfoRightBottom() const {
|
|
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
|
|
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
|
|
return QPoint(width() - skipx, height() - skipy);
|
|
}
|
|
|
|
bool Photo::isReadyForOpen() const {
|
|
ensureDataMediaCreated();
|
|
return _dataMedia->loaded();
|
|
}
|
|
|
|
void Photo::parentTextUpdated() {
|
|
_caption = (_parent->media() == this)
|
|
? createCaption(_parent->data())
|
|
: Ui::Text::String();
|
|
history()->owner().requestViewResize(_parent);
|
|
}
|
|
|
|
void Photo::showPhoto(FullMsgId id) {
|
|
_parent->delegate()->elementOpenPhoto(_data, id);
|
|
}
|
|
|
|
} // namespace HistoryView
|