ZaStoGram_desktop/Telegram/SourceFiles/editor/scene/scene_item_message.cpp
2026-10-08 20:04:47 +03:00

499 lines
12 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 "editor/scene/scene_item_message.h"
#include "data/data_document.h"
#include "editor/editor_audio_menu.h"
#include "editor/editor_message_render.h"
#include "editor/editor_message_source.h"
#include "editor/editor_message_video.h"
#include "editor/scene/scene.h"
#include "editor/video/video_segment_player.h"
#include "lang/lang_keys.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/popup_menu.h"
#include "window/themes/window_theme.h"
#include "styles/style_chat.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QCoreApplication>
#include <QGraphicsSceneMouseEvent>
namespace Editor {
namespace {
constexpr auto kMaxRatio = 8;
constexpr auto kPlaybackFrameSide = 1024;
[[nodiscard]] bool OnMainThread() {
return QThread::currentThread() == QCoreApplication::instance()->thread();
}
[[nodiscard]] int RatioFor(float64 width, int logicalWidth) {
return std::clamp(int(std::ceil(width / logicalWidth)), 1, kMaxRatio);
}
[[nodiscard]] QRect CoverRect(QRect hole, QSize video) {
if (video.isEmpty() || hole.isEmpty()) {
return hole;
}
const auto scale = std::max(
hole.width() / float64(video.width()),
hole.height() / float64(video.height()));
const auto size = QSize(
int(std::ceil(video.width() * scale)),
int(std::ceil(video.height() * scale)));
return QRect(
rect::center(hole) - QPoint(size.width() / 2, size.height() / 2),
size);
}
[[nodiscard]] QSize PlaybackFrameSize(QSize frames) {
const auto ratio = style::DevicePixelRatio();
const auto side = std::max(frames.width(), frames.height());
const auto fit = std::min(1., kPlaybackFrameSide / float64(side));
return QSize(
std::max(int(std::round(frames.width() * fit / ratio)), 1),
std::max(int(std::round(frames.height() * fit / ratio)), 1));
}
} // namespace
ItemMessage::ItemMessage(
std::shared_ptr<MessageSource> source,
std::unique_ptr<MessageRenderer> renderer,
ItemBase::Data data,
std::optional<bool> dark,
MessageVideoOptions video)
: ItemAnimated(std::move(data))
, _source(std::move(source))
, _renderer(std::move(renderer))
, _videoOptions(video)
, _dark(dark) {
attachRenderer();
watchVideo();
}
ItemMessage::~ItemMessage() = default;
void ItemMessage::attachRenderer() {
_renderer->setDark(_dark);
_renderer->setRepaintCallback([=] { scheduleRefresh(); });
setImage(_renderer->render(1), 1);
updateSize();
}
void ItemMessage::scheduleRefresh() {
if (_refreshScheduled) {
return;
}
_refreshScheduled = true;
crl::on_main(crl::guard(this, [=] {
_refreshScheduled = false;
refresh();
}));
}
void ItemMessage::refresh() {
if (!_renderer->ready()) {
return;
}
const auto ratio = std::max(_ratio, 1);
auto image = _renderer->render(ratio);
if (image.isNull()) {
return;
}
setImage(std::move(image), ratio);
updateSize();
update();
}
void ItemMessage::setImage(QImage image, int ratio) {
_image = std::move(image);
_image.setDevicePixelRatio(1.);
_ratio = _image.isNull() ? 0 : ratio;
_mediaRect = _renderer->mediaRect();
_mask = QImage();
_composite = QImage();
}
void ItemMessage::updateSize() {
const auto size = _renderer->size();
if (size.isEmpty() || (size == _size)) {
return;
}
_size = size;
setAspectRatio(_size.height() / float64(_size.width()));
}
int ItemMessage::neededRatio(not_null<QPainter*> p) const {
if (_size.isEmpty()) {
return 1;
}
const auto &transform = p->worldTransform();
const auto device = p->device();
const auto scale = std::hypot(transform.m11(), transform.m12())
* (device ? device->devicePixelRatio() : 1.);
return RatioFor(visibleRect().width() * scale, _size.width());
}
void ItemMessage::ensureRatio(int ratio) {
if (ratio <= _ratio || !_renderer->ready() || !OnMainThread()) {
return;
}
auto image = _renderer->render(ratio);
if (!image.isNull()) {
setImage(std::move(image), ratio);
}
}
void ItemMessage::watchVideo() {
_videoLifetime.destroy();
const auto video = _videoOptions.play ? _source->video() : nullptr;
if (!video) {
return;
}
video->changes(
) | rpl::on_next([=] {
checkVideo();
}, _videoLifetime);
video->load();
checkVideo();
}
void ItemMessage::checkVideo() {
const auto video = _source->video();
if (!video) {
return;
} else if (!isNormalStatus()) {
_radial = nullptr;
return;
} else if (!_clip) {
if (auto source = video->source()) {
createClip(std::move(source));
}
}
const auto loading = !_clip && video->loading();
if (loading && !_radial) {
_radial = std::make_unique<Ui::RadialAnimation>([=](crl::time now) {
updateRadial(now);
});
_radial->start(video->progress());
} else if (loading) {
_radial->update(video->progress(), false, crl::now());
} else {
_radial = nullptr;
}
update();
}
void ItemMessage::updateRadial(crl::time now) {
const auto video = _source->video();
if (!_radial || !video) {
return;
}
const auto loading = !_clip && video->loading();
_radial->update(video->progress(), !loading, now);
update();
}
void ItemMessage::createClip(std::shared_ptr<VideoClipSource> source) {
auto copy = std::make_shared<VideoClipSource>(*source);
copy->hasAudio = copy->hasAudio && _videoOptions.sound;
_clip = std::make_unique<VideoClip>(std::move(copy), [=] {
checkCutout();
update();
});
if (_playersReleased) {
_clip->stop();
}
notifyVideoClipChanged();
}
void ItemMessage::checkCutout() {
if (_cutout || !_clip || !_clip->player()->ready()) {
return;
}
_cutout = true;
update();
}
void ItemMessage::notifyVideoClipChanged() {
if (const auto owner = static_cast<Scene*>(scene())) {
owner->videoClipChanged(this);
}
}
QRect ItemMessage::holeRect() const {
return QRect(_mediaRect.topLeft() * _ratio, _mediaRect.size() * _ratio);
}
QRect ItemMessage::framesRect() const {
const auto hole = holeRect();
if (!_clip) {
return hole;
}
const auto thumbnail = _clip->source()->thumbnail.size();
const auto video = _source->video();
return CoverRect(
hole,
(!thumbnail.isEmpty()
? thumbnail
: video
? video->document()->dimensions
: QSize()));
}
Media::Encode::AnimatedEntity::Cutout ItemMessage::cutout() const {
const auto hole = holeRect();
if (_mask.isNull()) {
_mask = _renderer->videoMask(std::max(_ratio, 1));
}
return {
.picture = _image,
.mask = _mask,
.hole = hole,
.frames = framesRect(),
};
}
const QImage &ItemMessage::composeFrame() {
Expects(_clip != nullptr);
const auto hole = holeRect();
const auto frame = hole.isEmpty()
? QImage()
: _clip->frame(PlaybackFrameSize(framesRect().size()));
if (frame.isNull()) {
return _composite.isNull() ? _image : _composite;
}
return Media::Encode::ComposeCutout(cutout(), frame, _composite);
}
void ItemMessage::paintLoading(QPainter *p) const {
if (_mediaRect.isEmpty() || _size.isEmpty()) {
return;
}
const auto rect = visibleRect();
const auto scale = rect.width() / _size.width();
const auto center = rect.topLeft()
+ rect::center(QRectF(_mediaRect)) * scale;
const auto side = st::msgFileLayout.thumbSize * scale;
const auto line = st::msgFileRadialLine * scale;
const auto inner = QRectF(
center.x() - side / 2. + line,
center.y() - side / 2. + line,
side - 2 * line,
side - 2 * line);
PainterHighQualityEnabler hq(*p);
_radial->draw(*p, inner, line, st::historyFileThumbRadialFg);
}
void ItemMessage::save(SaveState state) {
ItemBase::save(state);
if (!_size.isEmpty()) {
ensureRatio(RatioFor(visibleRect().width(), _size.width()));
}
((state == SaveState::Keep) ? _kept : _saved) = _clip
? std::make_optional(_clip->state())
: std::nullopt;
}
void ItemMessage::restore(SaveState state) {
if (!hasState(state)) {
return;
}
ItemBase::restore(state);
const auto &saved = (state == SaveState::Keep) ? _kept : _saved;
if (_clip && saved) {
_clip->restore(*saved);
}
}
void ItemMessage::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
ensureRatio(neededRatio(p));
if (w) {
_playersReleased = false;
if (_clip) {
_clip->resume();
}
}
if (!_image.isNull()) {
const auto &image = (_cutout && w)
? composeFrame()
: (_cutout && !_composite.isNull())
? _composite
: _image;
p->setRenderHint(QPainter::SmoothPixmapTransform);
p->drawImage(visibleRect(), image);
}
if (_radial && w) {
paintLoading(p);
}
ItemBase::paint(p, option, w);
}
int ItemMessage::type() const {
return Type;
}
bool ItemMessage::animated() const {
return _cutout && _clip && _clip->animated();
}
bool ItemMessage::hasContent() const {
return _clip && _clip->hasContent();
}
QByteArray ItemMessage::content() const {
return _clip ? _clip->content() : QByteArray();
}
crl::time ItemMessage::loopDuration() const {
return _clip ? _clip->loopDuration() : 0;
}
VideoTrim ItemMessage::trim() const {
return _clip ? _clip->trim() : VideoTrim();
}
void ItemMessage::releasePlayers() {
_playersReleased = true;
if (_clip) {
_clip->stop();
}
}
void ItemMessage::setStatus(Status status) {
if (status != Status::Normal) {
releasePlayers();
}
ItemBase::setStatus(status);
if (status == Status::Normal) {
checkVideo();
}
}
VideoClip *ItemMessage::videoClip() {
return _clip.get();
}
Media::Encode::AnimatedEntity ItemMessage::animatedEntity(
const QTransform &sceneToCanvas) const {
auto result = ItemAnimated::animatedEntity(sceneToCanvas);
if (_cutout && !_image.isNull()) {
result.cutout = cutout();
}
return result;
}
Media::Encode::AnimatedEntity::Kind ItemMessage::entityKind() const {
return Media::Encode::AnimatedEntity::Kind::Webm;
}
QRectF ItemMessage::entityRect() const {
return visibleRect();
}
const std::shared_ptr<MessageSource> &ItemMessage::source() const {
return _source;
}
void ItemMessage::setSource(std::shared_ptr<MessageSource> source) {
_videoLifetime.destroy();
_clip = nullptr;
_radial = nullptr;
_cutout = false;
_source = std::move(source);
_renderer = std::make_unique<MessageRenderer>(_source);
_ratio = 0;
attachRenderer();
watchVideo();
notifyVideoClipChanged();
update();
}
std::optional<bool> ItemMessage::dark() const {
return _dark;
}
void ItemMessage::setDark(std::optional<bool> dark) {
if (_dark == dark) {
return;
}
_dark = dark;
_ratio = 0;
_renderer->setDark(dark);
refresh();
}
void ItemMessage::setEditCallback(EditCallback callback) {
_edit = std::move(callback);
}
bool ItemMessage::editable() const {
return _edit && _source->link().has_value();
}
QRectF ItemMessage::visibleRect() const {
return fittedRect(_size);
}
bool ItemMessage::flippable() const {
return false;
}
void ItemMessage::fillContextMenu(not_null<Ui::PopupMenu*> menu) {
const auto dark = _dark.value_or(Window::Theme::IsNightMode());
menu->addAction(
(dark
? tr::lng_settings_theme_day(tr::now)
: tr::lng_settings_theme_night(tr::now)),
[=] { setDark(!dark); },
&st::mediaMenuIconNightMode);
if (editable()) {
menu->addAction(
tr::lng_menu_formatting_link_edit(tr::now),
[=] { _edit(this); },
&st::mediaMenuIconEdit);
}
if (_clip && _clip->hasAudio()) {
AddVolumeAction(menu, _clip->volume(), [=](float64 volume) {
_clip->setVolume(volume);
});
}
}
void ItemMessage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
if (editable()) {
_edit(this);
} else {
ItemBase::mouseDoubleClickEvent(event);
}
}
std::shared_ptr<ItemBase> ItemMessage::duplicate(ItemBase::Data data) const {
auto result = std::make_shared<ItemMessage>(
_source,
std::make_unique<MessageRenderer>(_source),
std::move(data),
_dark,
_videoOptions);
result->_edit = _edit;
if (_clip && result->_clip) {
result->_clip->restore(_clip->state());
}
return result;
}
} // namespace Editor