Allow rotating content in media viewer.

This commit is contained in:
John Preston 2020-02-05 19:27:53 +04:00
parent 91244d5211
commit 4544b091a0
17 changed files with 289 additions and 89 deletions

View File

@ -337,6 +337,8 @@ PRIVATE
data/data_groups.h
data/data_location.cpp
data/data_location.h
data/data_media_rotation.cpp
data/data_media_rotation.h
data/data_media_types.cpp
data/data_media_types.h
data/data_messages.cpp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,49 @@
/*
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 "data/data_media_rotation.h"
namespace Data {
namespace {
[[nodiscard]] int NormalizeRotation(int rotation) {
const auto result = rotation
- ((rotation / 360) - ((rotation < 0) ? 1 : 0)) * 360;
Ensures(result >= 0 && result < 360);
return result;
}
} // namespace
void MediaRotation::set(not_null<PhotoData*> photo, int rotation) {
if (rotation % 360) {
_photoRotations[photo] = NormalizeRotation(rotation);
} else {
_photoRotations.remove(photo);
}
}
int MediaRotation::get(not_null<PhotoData*> photo) const {
const auto i = _photoRotations.find(photo);
return (i != end(_photoRotations)) ? i->second : 0;
}
void MediaRotation::set(not_null<DocumentData*> document, int rotation) {
if (rotation % 360) {
_documentRotations[document] = NormalizeRotation(rotation);
} else {
_documentRotations.remove(document);
}
}
int MediaRotation::get(not_null<DocumentData*> document) const {
const auto i = _documentRotations.find(document);
return (i != end(_documentRotations)) ? i->second : 0;
}
} // namespace Data

View File

@ -0,0 +1,29 @@
/*
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
*/
#pragma once
class PhotoData;
class DocumentData;
namespace Data {
class MediaRotation final {
public:
void set(not_null<PhotoData*> photo, int rotation);
[[nodiscard]] int get(not_null<PhotoData*> photo) const;
void set(not_null<DocumentData*> document, int rotation);
[[nodiscard]] int get(not_null<DocumentData*> document) const;
private:
base::flat_map<not_null<PhotoData*>, int> _photoRotations;
base::flat_map<not_null<DocumentData*>, int> _documentRotations;
};
} // namespace Data

View File

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_scheduled_messages.h"
#include "data/data_cloud_themes.h"
#include "data/data_streaming.h"
#include "data/data_media_rotation.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "base/call_delayed.h"
@ -192,7 +193,8 @@ Session::Session(not_null<Main::Session*> session)
, _groups(this)
, _scheduledMessages(std::make_unique<ScheduledMessages>(this))
, _cloudThemes(std::make_unique<CloudThemes>(session))
, _streaming(std::make_unique<Streaming>(this)) {
, _streaming(std::make_unique<Streaming>(this))
, _mediaRotation(std::make_unique<MediaRotation>()) {
_cache->open(Local::cacheKey());
_bigFileCache->open(Local::cacheBigFileKey());

View File

@ -60,6 +60,7 @@ class WallPaper;
class ScheduledMessages;
class CloudThemes;
class Streaming;
class MediaRotation;
class Session final {
public:
@ -92,6 +93,9 @@ public:
[[nodiscard]] Streaming &streaming() const {
return *_streaming;
}
[[nodiscard]] MediaRotation &mediaRotation() const {
return *_mediaRotation;
}
[[nodiscard]] MsgId nextNonHistoryEntryId() {
return ++_nonHistoryEntryId;
}
@ -984,6 +988,7 @@ private:
std::unique_ptr<ScheduledMessages> _scheduledMessages;
std::unique_ptr<CloudThemes> _cloudThemes;
std::unique_ptr<Streaming> _streaming;
std::unique_ptr<MediaRotation> _mediaRotation;
MsgId _nonHistoryEntryId = ServerMaxMsgId;
rpl::lifetime _lifetime;

View File

@ -104,6 +104,7 @@ mediaviewLeft: icon {{ "mediaview_next-flip_horizontal", mediaviewControlFg }};
mediaviewRight: icon {{ "mediaview_next", mediaviewControlFg }};
mediaviewClose: icon {{ "mediaview_close", mediaviewControlFg }};
mediaviewSave: icon {{ "mediaview_download", mediaviewControlFg }};
mediaviewRotate: icon {{ "mediaview_rotate", mediaviewControlFg }};
mediaviewMore: icon {{ "mediaview_more", mediaviewControlFg }};
mediaviewFileRed: icon {

View File

@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_file_origin.h"
#include "data/data_media_rotation.h"
#include "window/themes/window_theme_preview.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
@ -198,6 +199,32 @@ QPixmap PrepareStaticImage(const QString &path) {
return App::pixmapFromImageInPlace(std::move(image));
}
[[nodiscard]] QRect RotatedRect(QRect rect, int rotation) {
switch (rotation) {
case 0: return rect;
case 90: return QRect(
rect.y(),
-rect.x() - rect.width(),
rect.height(),
rect.width());
case 180: return QRect(
-rect.x() - rect.width(),
-rect.y() - rect.height(),
rect.width(),
rect.height());
case 270: return QRect(
-rect.y() - rect.height(),
rect.x(),
rect.height(),
rect.width());
}
Unexpected("Rotation in RotatedRect.");
}
[[nodiscard]] bool UsePainterRotation(int rotation) {
return Platform::IsMac() || !(rotation % 180);
}
} // namespace
struct OverlayWidget::SharedMedia {
@ -432,6 +459,12 @@ void OverlayWidget::moveToScreen(bool force) {
update();
}
QSize OverlayWidget::flipSizeByRotation(QSize size) const {
return (((_rotation / 90) % 2) == 1)
? QSize(size.height(), size.width())
: size;
}
bool OverlayWidget::videoShown() const {
return _streamed && !_streamed->instance.info().video.cover.isNull();
}
@ -439,7 +472,7 @@ bool OverlayWidget::videoShown() const {
QSize OverlayWidget::videoSize() const {
Expects(videoShown());
return _streamed->instance.info().video.size;
return flipSizeByRotation(_streamed->instance.info().video.size);
}
bool OverlayWidget::videoIsGifv() const {
@ -498,7 +531,7 @@ QImage OverlayWidget::videoFrameForDirectPaint() const {
}
bool OverlayWidget::documentContentShown() const {
return _doc && (!_current.isNull() || videoShown());
return _doc && (!_staticContent.isNull() || videoShown());
}
bool OverlayWidget::documentBubbleShown() const {
@ -506,7 +539,7 @@ bool OverlayWidget::documentBubbleShown() const {
|| (_doc
&& !_themePreviewShown
&& !_streamed
&& _current.isNull());
&& _staticContent.isNull());
}
void OverlayWidget::clearStreaming(bool savePosition) {
@ -630,8 +663,10 @@ void OverlayWidget::updateControls() {
|| (_doc
&& _doc->filepath(DocumentData::FilePathResolve::Checked).isEmpty()
&& !_doc->loading());
_saveNav = myrtlrect(width() - st::mediaviewIconSize.width() * 2, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
_saveNav = myrtlrect(width() - st::mediaviewIconSize.width() * 3, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
_rotateNav = myrtlrect(width() - st::mediaviewIconSize.width() * 2, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);
_moreNav = myrtlrect(width() - st::mediaviewIconSize.width(), height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height());
_moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore);
@ -826,6 +861,7 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) {
+ (_over == OverRightNav ? _rightNav : _rightNavIcon)
+ (_over == OverClose ? _closeNav : _closeNavIcon)
+ _saveNavIcon
+ _rotateNavIcon
+ _moreNavIcon
+ _headerNav
+ _nameNav
@ -848,6 +884,15 @@ void OverlayWidget::updateCursor() {
: (_over == OverNone ? style::cur_default : style::cur_pointer));
}
int OverlayWidget::contentRotation() const {
if (!_streamed) {
return _rotation;
}
return (_rotation + (_streamed
? _streamed->instance.info().video.rotation
: 0)) % 360;
}
QRect OverlayWidget::contentRect() const {
return { _x, _y, _w, _h };
}
@ -1410,12 +1455,9 @@ void OverlayWidget::onOverview() {
void OverlayWidget::onCopy() {
_dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow);
if (_doc) {
if (videoShown()) {
QGuiApplication::clipboard()->setImage(
transformVideoFrame(videoFrame()));
} else if (!_current.isNull()) {
QGuiApplication::clipboard()->setPixmap(_current);
}
QGuiApplication::clipboard()->setImage(videoShown()
? transformVideoFrame(videoFrame())
: transformStaticContent(_staticContent));
} else if (_photo && _photo->loaded()) {
QGuiApplication::clipboard()->setPixmap(_photo->large()->pix(fileOrigin()));
}
@ -1899,6 +1941,7 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
_doc = nullptr;
_fullScreenVideo = false;
_photo = photo;
_rotation = _photo->owner().mediaRotation().get(_photo);
_radial.stop();
refreshMediaViewer();
@ -1907,10 +1950,13 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
_zoom = 0;
_zoomToScreen = _zoomToDefault = 0;
_blurred = true;
_current = QPixmap();
_staticContent = QPixmap();
_down = OverNone;
_w = style::ConvertScale(photo->width());
_h = style::ConvertScale(photo->height());
const auto size = style::ConvertScale(flipSizeByRotation(QSize(
photo->width(),
photo->height())));
_w = size.width();
_h = size.height();
contentSizeChanged();
refreshFromLabel(item);
_photo->download(fileOrigin());
@ -1948,10 +1994,11 @@ void OverlayWidget::displayDocument(
moveToScreen();
}
_fullScreenVideo = false;
_current = QPixmap();
_staticContent = QPixmap();
clearStreaming(_doc != doc);
destroyThemePreview();
_doc = doc;
_rotation = _doc ? _doc->owner().mediaRotation().get(_doc) : 0;
_themeCloudData = cloud;
_photo = nullptr;
_radial.stop();
@ -1960,9 +2007,9 @@ void OverlayWidget::displayDocument(
if (_doc) {
if (_doc->sticker()) {
if (const auto image = _doc->getStickerLarge()) {
_current = image->pix(fileOrigin());
_staticContent = image->pix(fileOrigin());
} else if (_doc->hasThumbnail()) {
_current = _doc->thumbnail()->pixBlurred(
_staticContent = _doc->thumbnail()->pixBlurred(
fileOrigin(),
_doc->dimensions.width(),
_doc->dimensions.height());
@ -1981,7 +2028,7 @@ void OverlayWidget::displayDocument(
if (location.accessEnable()) {
const auto &path = location.name();
if (QImageReader(path).canRead()) {
_current = PrepareStaticImage(path);
_staticContent = PrepareStaticImage(path);
}
}
location.accessDisable();
@ -2045,10 +2092,12 @@ void OverlayWidget::displayDocument(
_docIconRect = myrtlrect(_docRect.x() + st::mediaviewFilePadding, _docRect.y() + st::mediaviewFilePadding, st::mediaviewFileIconSize, st::mediaviewFileIconSize);
} else if (_themePreviewShown) {
updateThemePreviewGeometry();
} else if (!_current.isNull()) {
_current.setDevicePixelRatio(cRetinaFactor());
_w = style::ConvertScale(_current.width());
_h = style::ConvertScale(_current.height());
} else if (!_staticContent.isNull()) {
_staticContent.setDevicePixelRatio(cRetinaFactor());
const auto size = style::ConvertScale(
flipSizeByRotation(_staticContent.size()));
_w = size.width();
_h = size.height();
} else if (videoShown()) {
const auto contentSize = style::ConvertScale(videoSize());
_w = contentSize.width();
@ -2175,7 +2224,7 @@ void OverlayWidget::initStreamingThumbnail() {
const auto h = size.height();
const auto options = VideoThumbOptions(_doc);
const auto goodOptions = (options & ~Images::Option::Blurred);
_current = (useGood
_staticContent = (useGood
? good
: useThumb
? thumb
@ -2188,20 +2237,24 @@ void OverlayWidget::initStreamingThumbnail() {
useGood ? goodOptions : options,
w / cIntRetinaFactor(),
h / cIntRetinaFactor());
_current.setDevicePixelRatio(cRetinaFactor());
_staticContent.setDevicePixelRatio(cRetinaFactor());
}
void OverlayWidget::streamingReady(Streaming::Information &&info) {
if (videoShown()) {
const auto contentSize = style::ConvertScale(videoSize());
if (contentSize != QSize(_width, _height)) {
update(contentRect());
_w = contentSize.width();
_h = contentSize.height();
contentSizeChanged();
}
applyVideoSize();
}
update(contentRect());
}
void OverlayWidget::applyVideoSize() {
const auto contentSize = style::ConvertScale(videoSize());
if (contentSize != QSize(_width, _height)) {
update(contentRect());
_w = contentSize.width();
_h = contentSize.height();
contentSizeChanged();
}
this->update(contentRect());
}
bool OverlayWidget::createStreamingObjects() {
@ -2234,20 +2287,32 @@ bool OverlayWidget::createStreamingObjects() {
QImage OverlayWidget::transformVideoFrame(QImage frame) const {
Expects(videoShown());
if (_streamed->instance.info().video.rotation != 0) {
const auto rotation = contentRotation();
if (rotation != 0) {
auto transform = QTransform();
transform.rotate(_streamed->instance.info().video.rotation);
transform.rotate(rotation);
frame = frame.transformed(transform);
}
if (frame.size() != _streamed->instance.info().video.size) {
const auto requiredSize = videoSize();
if (frame.size() != requiredSize) {
frame = frame.scaled(
_streamed->instance.info().video.size,
requiredSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
return frame;
}
QImage OverlayWidget::transformStaticContent(QPixmap content) const {
auto image = content.toImage();
if (!_rotation) {
return image;
}
auto transform = QTransform();
transform.rotate(_rotation);
return image.transformed(transform);
}
void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
using namespace Streaming;
@ -2420,6 +2485,25 @@ void OverlayWidget::playbackControlsToPictureInPicture() {
}
}
void OverlayWidget::playbackControlsRotate() {
if (_photo) {
auto &storage = _photo->owner().mediaRotation();
storage.set(_photo, storage.get(_photo) - 90);
_rotation = storage.get(_photo);
redisplayContent();
} else if (_doc) {
auto &storage = _doc->owner().mediaRotation();
storage.set(_doc, storage.get(_doc) - 90);
_rotation = storage.get(_doc);
if (videoShown()) {
applyVideoSize();
update(contentRect());
} else {
redisplayContent();
}
}
}
void OverlayWidget::playbackPauseResume() {
Expects(_streamed != nullptr);
@ -2449,7 +2533,9 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) {
if (videoShown()) {
_streamed->instance.saveFrameToCover();
_current = Images::PixmapFast(transformVideoFrame(videoFrame()));
const auto saved = base::take(_rotation);
_staticContent = Images::PixmapFast(transformVideoFrame(videoFrame()));
_rotation = saved;
update(contentRect());
}
auto options = Streaming::PlaybackOptions();
@ -2625,18 +2711,18 @@ void OverlayWidget::validatePhotoImage(Image *image, bool blurred) {
image->load(fileOrigin());
}
return;
} else if (!_current.isNull() && (blurred || !_blurred)) {
} else if (!_staticContent.isNull() && (blurred || !_blurred)) {
return;
}
const auto w = _width * cIntRetinaFactor();
const auto h = _height * cIntRetinaFactor();
_current = image->pixNoCache(
_staticContent = image->pixNoCache(
fileOrigin(),
w,
h,
Images::Option::Smooth
| (blurred ? Images::Option::Blurred : Images::Option(0)));
_current.setDevicePixelRatio(cRetinaFactor());
_staticContent.setDevicePixelRatio(cRetinaFactor());
_blurred = blurred;
}
@ -2645,7 +2731,7 @@ void OverlayWidget::validatePhotoCurrentImage() {
validatePhotoImage(_photo->thumbnail(), true);
validatePhotoImage(_photo->thumbnailSmall(), true);
validatePhotoImage(_photo->thumbnailInline(), true);
if (_current.isNull()
if (_staticContent.isNull()
&& _peer
&& !_msgid
&& _peer->userpicLoaded()
@ -2654,7 +2740,7 @@ void OverlayWidget::validatePhotoCurrentImage() {
Images::Create(_peer->userpicLocation()).get(),
true);
}
if (_current.isNull()) {
if (_staticContent.isNull()) {
_photo->loadThumbnailSmall(fileOrigin());
}
}
@ -2697,14 +2783,7 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
if (videoShown()) {
paintTransformedVideoFrame(p);
} else {
if ((!_doc || !_doc->getStickerLarge())
&& (_current.isNull() || _current.hasAlpha())) {
p.fillRect(rect, _transparentBrush);
}
if (!_current.isNull()) {
PainterHighQualityEnabler hq(p);
p.drawPixmap(rect, _current);
}
paintTransformedStaticContent(p);
}
const auto radial = _radial.animating();
@ -2834,6 +2913,13 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
st::mediaviewSave.paintInCenter(p, _saveNavIcon);
}
// rotate button
if (_rotateNavIcon.intersects(r)) {
auto o = overLevel(OverRotate);
p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co);
st::mediaviewRotate.paintInCenter(p, _rotateNavIcon);
}
// more area
if (_moreNavIcon.intersects(r)) {
auto o = overLevel(OverMore);
@ -2927,46 +3013,52 @@ void OverlayWidget::paintTransformedVideoFrame(Painter &p) {
const auto rect = contentRect();
const auto image = videoFrameForDirectPaint();
//if (_fullScreenVideo) {
// const auto fill = rect.intersected(this->rect());
// PaintImageProfile(p, image, rect, fill);
//} else {
const auto rotation = _streamed->instance.info().video.rotation;
const auto rotated = [](QRect rect, int rotation) {
switch (rotation) {
case 0: return rect;
case 90: return QRect(
rect.y(),
-rect.x() - rect.width(),
rect.height(),
rect.width());
case 180: return QRect(
-rect.x() - rect.width(),
-rect.y() - rect.height(),
rect.width(),
rect.height());
case 270: return QRect(
-rect.y() - rect.height(),
rect.x(),
rect.height(),
rect.width());
}
Unexpected("Rotation in OverlayWidget::paintTransformedVideoFrame");
};
PainterHighQualityEnabler hq(p);
if (rotation) {
p.save();
p.rotate(rotation);
}
p.drawImage(rotated(rect, rotation), image);
if (rotation) {
p.restore();
const auto rotation = contentRotation();
if (UsePainterRotation(rotation)) {
if (rotation) {
p.save();
p.rotate(rotation);
}
p.drawImage(RotatedRect(rect, rotation), image);
if (rotation) {
p.restore();
}
} else {
p.drawImage(rect, transformVideoFrame(image));
}
if (_streamed->instance.player().ready()) {
_streamed->instance.markFrameShown();
}
//}
}
void OverlayWidget::paintTransformedStaticContent(Painter &p) {
const auto rect = contentRect();
PainterHighQualityEnabler hq(p);
if ((!_doc || !_doc->getStickerLarge())
&& (_staticContent.isNull()
|| _staticContent.hasAlpha())) {
p.fillRect(rect, _transparentBrush);
}
if (_staticContent.isNull()) {
return;
}
const auto rotation = contentRotation();
if (UsePainterRotation(rotation)) {
if (rotation) {
p.save();
p.rotate(rotation);
}
p.drawPixmap(RotatedRect(rect, rotation), _staticContent);
if (rotation) {
p.restore();
}
} else {
p.drawImage(rect, transformStaticContent(_staticContent));
}
}
void OverlayWidget::paintRadialLoading(
@ -3438,6 +3530,7 @@ void OverlayWidget::mousePressEvent(QMouseEvent *e) {
|| _over == OverDate
|| _over == OverHeader
|| _over == OverSave
|| _over == OverRotate
|| _over == OverIcon
|| _over == OverMore
|| _over == OverClose
@ -3515,6 +3608,7 @@ void OverlayWidget::updateOverRect(OverState state) {
case OverName: update(_nameNav); break;
case OverDate: update(_dateNav); break;
case OverSave: update(_saveNavIcon); break;
case OverRotate: update(_rotateNavIcon); break;
case OverIcon: update(_docIconRect); break;
case OverHeader: update(_headerNav); break;
case OverClose: update(_closeNav); break;
@ -3608,6 +3702,8 @@ void OverlayWidget::updateOver(QPoint pos) {
updateOverState(OverHeader);
} else if (_saveVisible && _saveNav.contains(pos)) {
updateOverState(OverSave);
} else if (_rotateNav.contains(pos)) {
updateOverState(OverRotate);
} else if (_doc && documentBubbleShown() && _docIconRect.contains(pos)) {
updateOverState(OverIcon);
} else if (_moreNav.contains(pos)) {
@ -3650,6 +3746,8 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) {
onOverview();
} else if (_over == OverSave && _down == OverSave) {
onDownload();
} else if (_over == OverRotate && _down == OverRotate) {
playbackControlsRotate();
} else if (_over == OverIcon && _down == OverIcon) {
onDocClick();
} else if (_over == OverMore && _down == OverMore) {
@ -3872,7 +3970,7 @@ void OverlayWidget::setVisibleHook(bool visible) {
clearStreaming();
destroyThemePreview();
_radial.stop();
_current = QPixmap();
_staticContent = QPixmap();
_themePreview = nullptr;
_themeApply.destroyDelayed();
_themeCancel.destroyDelayed();

View File

@ -140,6 +140,7 @@ private:
OverName,
OverDate,
OverSave,
OverRotate,
OverMore,
OverIcon,
OverVideo,
@ -180,6 +181,7 @@ private:
void playbackControlsToFullScreen() override;
void playbackControlsFromFullScreen() override;
void playbackControlsToPictureInPicture() override;
void playbackControlsRotate() override;
void playbackPauseResume();
void playbackToggleFullScreen();
void playbackPauseOnCall();
@ -283,7 +285,8 @@ private:
void documentUpdated(DocumentData *doc);
void changingMsgId(not_null<HistoryItem*> row, MsgId newId);
QRect contentRect() const;
[[nodiscard]] int contentRotation() const;
[[nodiscard]] QRect contentRect() const;
void contentSizeChanged();
// Radial animation interface.
@ -325,21 +328,27 @@ private:
void validatePhotoImage(Image *image, bool blurred);
void validatePhotoCurrentImage();
[[nodiscard]] QSize flipSizeByRotation(QSize size) const;
void applyVideoSize();
[[nodiscard]] bool videoShown() const;
[[nodiscard]] QSize videoSize() const;
[[nodiscard]] bool videoIsGifv() const;
[[nodiscard]] QImage videoFrame() const;
[[nodiscard]] QImage videoFrameForDirectPaint() const;
[[nodiscard]] QImage transformVideoFrame(QImage frame) const;
[[nodiscard]] QImage transformStaticContent(QPixmap content) const;
[[nodiscard]] bool documentContentShown() const;
[[nodiscard]] bool documentBubbleShown() const;
void paintTransformedVideoFrame(Painter &p);
void paintTransformedStaticContent(Painter &p);
void clearStreaming(bool savePosition = true);
QBrush _transparentBrush;
PhotoData *_photo = nullptr;
DocumentData *_doc = nullptr;
int _rotation = 0;
std::unique_ptr<SharedMedia> _sharedMedia;
std::optional<SharedMediaWithLastSlice> _sharedMediaData;
std::optional<SharedMediaWithLastSlice::Key> _sharedMediaDataKey;
@ -351,7 +360,7 @@ private:
QRect _closeNav, _closeNavIcon;
QRect _leftNav, _leftNavIcon, _rightNav, _rightNavIcon;
QRect _headerNav, _nameNav, _dateNav;
QRect _saveNav, _saveNavIcon, _moreNav, _moreNavIcon;
QRect _rotateNav, _rotateNavIcon, _saveNav, _saveNavIcon, _moreNav, _moreNavIcon;
bool _leftNavVisible = false;
bool _rightNavVisible = false;
bool _saveVisible = false;
@ -382,7 +391,7 @@ private:
QPoint _mStart;
bool _pressed = false;
int32 _dragging = 0;
QPixmap _current;
QPixmap _staticContent;
bool _blurred = true;
std::unique_ptr<Streamed> _streamed;

View File

@ -223,6 +223,10 @@ void PlaybackControls::showMenu() {
addSpeed(1.75);
addSpeed(2.);
_menu.emplace(this, st::mediaviewControlsPopupMenu);
_menu->addAction("Rotate video", [=] {
_delegate->playbackControlsRotate();
});
_menu->addSeparator();
_menu->addAction(
tr::lng_mediaview_playback_speed(tr::now),
std::move(submenu));

View File

@ -46,6 +46,7 @@ public:
virtual void playbackControlsToFullScreen() = 0;
virtual void playbackControlsFromFullScreen() = 0;
virtual void playbackControlsToPictureInPicture() = 0;
virtual void playbackControlsRotate() = 0;
};
PlaybackControls(QWidget *parent, not_null<Delegate*> delegate);

@ -1 +1 @@
Subproject commit 1888853b52291c32ce5bbca7212e67c262c76cee
Subproject commit 628d3b9ab6443acd352617ac78cc3131ba41dbbc