mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
925 lines
23 KiB
C++
925 lines
23 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/video/video_editor.h"
|
|
|
|
#include "base/timer.h"
|
|
#include "editor/editor_crop.h"
|
|
#include "editor/video/video_timeline.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "media/streaming/media_streaming_document.h"
|
|
#include "media/streaming/media_streaming_instance.h"
|
|
#include "media/streaming/media_streaming_loader_local.h"
|
|
#include "media/streaming/media_streaming_player.h"
|
|
#include "media/view/media_view_pip.h"
|
|
#include "ui/effects/ripple_animation.h"
|
|
#include "ui/layers/layer_widget.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "ui/painter.h"
|
|
#include "styles/style_editor.h"
|
|
|
|
#include <QtGui/QPainterPath>
|
|
#include <QtGui/QtEvents>
|
|
#include <QtWidgets/QApplication>
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
using Media::View::FlipSizeByRotation;
|
|
|
|
constexpr auto kSeekThrottle = crl::time(120);
|
|
|
|
constexpr auto kBubbleMinVisible = crl::time(600);
|
|
|
|
constexpr auto kBubbleDuration = crl::time(500);
|
|
|
|
constexpr auto kCaptureDuration = crl::time(420);
|
|
|
|
class ControlsBar final : public Ui::RpWidget {
|
|
public:
|
|
using RpWidget::RpWidget;
|
|
|
|
void layoutChildren();
|
|
|
|
private:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
};
|
|
|
|
void ControlsBar::paintEvent(QPaintEvent *e) {
|
|
auto p = QPainter(this);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto radius = std::min(width(), height()) / 2.;
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(st::roundedBg);
|
|
p.drawRoundedRect(rect(), radius, radius);
|
|
}
|
|
|
|
void ControlsBar::layoutChildren() {
|
|
auto widgets = std::vector<QWidget*>();
|
|
for (const auto child : children()) {
|
|
if (child->isWidgetType()) {
|
|
widgets.push_back(static_cast<QWidget*>(child));
|
|
}
|
|
}
|
|
if (widgets.empty() || width() <= 0) {
|
|
return;
|
|
}
|
|
auto total = 0;
|
|
for (const auto widget : widgets) {
|
|
total += widget->width();
|
|
}
|
|
const auto count = int(widgets.size());
|
|
const auto step = (count > 1)
|
|
? ((width() - total) / float64(count - 1))
|
|
: 0.;
|
|
auto left = 0.;
|
|
for (const auto widget : widgets) {
|
|
widget->move(
|
|
int(base::SafeRound(left)),
|
|
(height() - widget->height()) / 2);
|
|
left += widget->width() + step;
|
|
}
|
|
}
|
|
|
|
class BarTextButton final : public Ui::RippleButton {
|
|
public:
|
|
BarTextButton(
|
|
not_null<Ui::RpWidget*> parent,
|
|
const QString &text,
|
|
const style::color &fg);
|
|
|
|
private:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
[[nodiscard]] QImage prepareRippleMask() const override;
|
|
|
|
const style::color &_fg;
|
|
const QString _text;
|
|
|
|
};
|
|
|
|
BarTextButton::BarTextButton(
|
|
not_null<Ui::RpWidget*> parent,
|
|
const QString &text,
|
|
const style::color &fg)
|
|
: RippleButton(parent, st::photoEditorRotateButton.ripple)
|
|
, _fg(fg)
|
|
, _text(text) {
|
|
const auto &padding = st::photoEditorTextButtonPadding;
|
|
resize(
|
|
st::photoEditorButtonStyle.font->width(_text)
|
|
+ padding.left()
|
|
+ padding.right(),
|
|
st::photoEditorButtonBarHeight);
|
|
}
|
|
|
|
QImage BarTextButton::prepareRippleMask() const {
|
|
return Ui::RippleAnimation::RoundRectMask(size(), height() / 2);
|
|
}
|
|
|
|
void BarTextButton::paintEvent(QPaintEvent *e) {
|
|
auto p = QPainter(this);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
if (isOver() || isDown()) {
|
|
const auto radius = height() / 2.;
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(st::photoEditorEdgeButtonBg);
|
|
p.drawRoundedRect(rect(), radius, radius);
|
|
}
|
|
paintRipple(p, 0, 0);
|
|
p.setPen(_fg);
|
|
p.setFont(st::photoEditorButtonStyle.font);
|
|
p.drawText(rect(), Qt::AlignCenter, _text);
|
|
}
|
|
|
|
[[nodiscard]] float64 ShapeRadius(
|
|
QSizeF size,
|
|
EditorData::CropType type,
|
|
RoundedCornersLevel corners) {
|
|
const auto shorter = std::min(size.width(), size.height());
|
|
if (type == EditorData::CropType::Ellipse) {
|
|
return shorter / 2.;
|
|
}
|
|
const auto multiplier = (type == EditorData::CropType::RoundedRect)
|
|
? RoundedCornersMultiplier(corners)
|
|
: RoundedCornersMultiplier(RoundedCornersLevel::Small);
|
|
return shorter * multiplier;
|
|
}
|
|
|
|
[[nodiscard]] QPainterPath ShapePath(
|
|
const QRectF &rect,
|
|
EditorData::CropType type,
|
|
RoundedCornersLevel corners,
|
|
float64 rounding = 1.) {
|
|
auto result = QPainterPath();
|
|
if (rect.isEmpty()) {
|
|
return result;
|
|
}
|
|
const auto radius = ShapeRadius(rect.size(), type, corners) * rounding;
|
|
if (radius <= 0.) {
|
|
result.addRect(rect);
|
|
} else {
|
|
result.addRoundedRect(rect, radius, radius);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Media::Streaming::FrameRequest FrameRequestFor(QSize size) {
|
|
auto result = Media::Streaming::FrameRequest();
|
|
result.resize = size * style::DevicePixelRatio();
|
|
result.outer = result.resize;
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
VideoEditor::VideoEditor(
|
|
not_null<QWidget*> parent,
|
|
VideoEditorDescriptor descriptor)
|
|
: RpWidget(parent)
|
|
, _path(descriptor.path)
|
|
, _dimensions(descriptor.dimensions)
|
|
, _duration(std::max(descriptor.duration, crl::time(1)))
|
|
, _data(descriptor.data) {
|
|
_geometry.cropType = _data.editor.cropType;
|
|
_geometry.cropMode = _data.editor.cropMode;
|
|
_geometry.crop = [&] {
|
|
const auto side = std::min(_dimensions.width(), _dimensions.height());
|
|
return QRect(
|
|
(_dimensions.width() - side) / 2,
|
|
(_dimensions.height() - side) / 2,
|
|
side,
|
|
side);
|
|
}();
|
|
|
|
_crop = base::make_unique_q<Crop>(
|
|
this,
|
|
_geometry,
|
|
_dimensions,
|
|
_data.editor);
|
|
|
|
setupControls();
|
|
setupTimeline();
|
|
setupStreaming();
|
|
setupTapToPause();
|
|
refreshCoverPreview();
|
|
|
|
_crop->events(
|
|
) | rpl::filter([](not_null<QEvent*> e) {
|
|
return (e->type() == QEvent::MouseButtonRelease);
|
|
}) | rpl::on_next([=] {
|
|
invalidateCoverPreview();
|
|
}, _crop->lifetime());
|
|
|
|
sizeValue(
|
|
) | rpl::filter([=](QSize size) {
|
|
return !size.isEmpty();
|
|
}) | rpl::on_next([=] {
|
|
applyGeometry();
|
|
}, lifetime());
|
|
|
|
paintRequest(
|
|
) | rpl::on_next([=] {
|
|
auto p = QPainter(this);
|
|
paint(p);
|
|
}, lifetime());
|
|
}
|
|
|
|
VideoEditor::~VideoEditor() = default;
|
|
|
|
void VideoEditor::setupTimeline() {
|
|
// A sibling of the controls container would never see a mouse press.
|
|
_timeline = base::make_unique_q<VideoTimeline>(
|
|
_controls.get(),
|
|
VideoTimelineDescriptor{
|
|
.path = _path,
|
|
.dimensions = _dimensions,
|
|
.duration = _duration,
|
|
.maxDuration = _data.maxDuration,
|
|
.minDuration = _data.minDuration,
|
|
});
|
|
_from = _timeline->from();
|
|
_till = _timeline->till();
|
|
_cover = _timeline->cover();
|
|
_position = _from;
|
|
|
|
struct State {
|
|
base::Timer timer;
|
|
crl::time pending = -1;
|
|
};
|
|
const auto state = lifetime().make_state<State>();
|
|
state->timer.setCallback([=] {
|
|
if (state->pending >= 0) {
|
|
const auto position = state->pending;
|
|
state->pending = -1;
|
|
restart(position);
|
|
}
|
|
});
|
|
const auto seek = [=](crl::time position) {
|
|
state->pending = position;
|
|
if (!state->timer.isActive()) {
|
|
state->timer.callOnce(kSeekThrottle);
|
|
}
|
|
};
|
|
|
|
_timeline->trimChanges(
|
|
) | rpl::on_next([=](crl::time edge) {
|
|
_from = _timeline->from();
|
|
_till = _timeline->till();
|
|
_cover = _timeline->cover();
|
|
seek(edge);
|
|
}, _timeline->lifetime());
|
|
|
|
_timeline->coverChanges(
|
|
) | rpl::on_next([=](crl::time cover) {
|
|
_cover = cover;
|
|
seek(cover);
|
|
updateBubble();
|
|
refreshCoverPreview();
|
|
}, _timeline->lifetime());
|
|
|
|
_timeline->draggingChanges(
|
|
) | rpl::on_next([=](bool dragging) {
|
|
_dragging = dragging;
|
|
if (dragging) {
|
|
_draggingHead = _timeline->draggingHead();
|
|
if (_instance) {
|
|
_instance->pause();
|
|
}
|
|
} else {
|
|
state->timer.cancel();
|
|
const auto latest = (state->pending >= 0)
|
|
? state->pending
|
|
: _position;
|
|
state->pending = -1;
|
|
const auto resume = std::clamp(latest, _from, _till);
|
|
restart((resume >= _till) ? _from : resume);
|
|
}
|
|
if (!dragging && _draggingHead) {
|
|
_draggingHead = false;
|
|
startCapture();
|
|
return;
|
|
}
|
|
updateBubble();
|
|
}, _timeline->lifetime());
|
|
}
|
|
|
|
void VideoEditor::setupControls() {
|
|
_controls = base::make_unique_q<Ui::RpWidget>(this);
|
|
_bar = base::make_unique_q<ControlsBar>(_controls.get());
|
|
const auto bar = _bar.get();
|
|
|
|
_cancelButton = base::make_unique_q<BarTextButton>(
|
|
bar,
|
|
tr::lng_cancel(tr::now),
|
|
st::mediaviewCaptionFg);
|
|
_rotate = base::make_unique_q<Ui::IconButton>(
|
|
bar,
|
|
st::photoEditorRotateButton);
|
|
_flip = base::make_unique_q<Ui::IconButton>(
|
|
bar,
|
|
st::photoEditorFlipButton);
|
|
_confirm = base::make_unique_q<BarTextButton>(
|
|
bar,
|
|
(_data.editor.confirm.isEmpty()
|
|
? tr::lng_box_done(tr::now)
|
|
: _data.editor.confirm),
|
|
st::mediaviewTextLinkFg);
|
|
|
|
if (!_data.hint.isEmpty()) {
|
|
_hint = base::make_unique_q<Ui::FlatLabel>(
|
|
_controls.get(),
|
|
rpl::single(_data.hint),
|
|
st::videoEditorHint);
|
|
_hint->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
}
|
|
|
|
_bubble = base::make_unique_q<Ui::RpWidget>(this);
|
|
_bubble->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
_bubble->hide();
|
|
_bubble->paintRequest(
|
|
) | rpl::on_next([=] {
|
|
const auto shown = _bubbleShown.value(_bubbleVisible ? 1. : 0.);
|
|
if (_bubblePreview.isNull() || shown <= 0.) {
|
|
return;
|
|
}
|
|
auto p = QPainter(_bubble.get());
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setOpacity(shown);
|
|
|
|
const auto size = QSizeF(bubbleSize());
|
|
const auto dotY = float64(_bubble->height());
|
|
const auto grown = QSizeF(
|
|
size.width() * shown,
|
|
size.height() * shown);
|
|
const auto centre = QPointF(
|
|
size.width() / 2.,
|
|
dotY + (size.height() / 2. - dotY) * shown);
|
|
const auto rect = QRectF(
|
|
centre.x() - grown.width() / 2.,
|
|
centre.y() - grown.height() / 2.,
|
|
grown.width(),
|
|
grown.height());
|
|
const auto type = _geometry.cropType;
|
|
const auto corners = _geometry.cornersLevel;
|
|
p.setClipPath(ShapePath(rect, type, corners));
|
|
p.drawImage(rect, _bubblePreview);
|
|
p.setClipping(false);
|
|
p.setBrush(Qt::NoBrush);
|
|
const auto border = st::videoEditorBubbleBorder;
|
|
const auto half = border / 2.;
|
|
p.setPen(QPen(st::mediaviewControlFg, border));
|
|
p.drawPath(ShapePath(
|
|
rect.adjusted(half, half, -half, -half),
|
|
type,
|
|
corners));
|
|
}, _bubble->lifetime());
|
|
|
|
_bubbleHideTimer.setCallback([=] { updateBubble(); });
|
|
|
|
_capture = base::make_unique_q<Ui::RpWidget>(this);
|
|
_capture->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
_capture->hide();
|
|
_capture->paintRequest(
|
|
) | rpl::on_next([=] {
|
|
if (_bubblePreview.isNull()) {
|
|
return;
|
|
}
|
|
const auto value = _captureShown.value(1.);
|
|
auto p = QPainter(_capture.get());
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
|
|
const auto from = captureFrom();
|
|
|
|
const auto to = QRect(_bubble->pos(), bubbleSize());
|
|
if (from.isEmpty() || to.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto lerp = [&](int a, int b) {
|
|
return int(base::SafeRound(a + (b - a) * value));
|
|
};
|
|
const auto rect = QRectF(QRect(
|
|
lerp(from.x(), to.x()),
|
|
lerp(from.y(), to.y()),
|
|
lerp(from.width(), to.width()),
|
|
lerp(from.height(), to.height())));
|
|
const auto type = _geometry.cropType;
|
|
const auto corners = _geometry.cornersLevel;
|
|
p.setClipPath(ShapePath(rect, type, corners, value));
|
|
p.drawImage(rect, _bubblePreview);
|
|
p.setClipping(false);
|
|
|
|
p.setOpacity(value * value);
|
|
p.setBrush(Qt::NoBrush);
|
|
const auto border = st::videoEditorBubbleBorder;
|
|
const auto half = border / 2.;
|
|
p.setPen(QPen(st::mediaviewControlFg, border));
|
|
p.drawPath(ShapePath(
|
|
rect.adjusted(half, half, -half, -half),
|
|
type,
|
|
corners,
|
|
value));
|
|
p.setOpacity(1.);
|
|
|
|
const auto flash = std::clamp(1. - value * 3., 0., 1.);
|
|
if (flash > 0. && !from.isEmpty()) {
|
|
p.setOpacity(flash);
|
|
p.fillRect(from, st::videoEditorFlashFg);
|
|
}
|
|
}, _capture->lifetime());
|
|
|
|
if (!_data.editor.about.text.isEmpty()) {
|
|
_about = base::make_unique_q<Ui::FlatLabel>(
|
|
this,
|
|
rpl::single(_data.editor.about),
|
|
st::videoEditorAbout);
|
|
_about->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
}
|
|
|
|
_rotate->setClickedCallback([=] {
|
|
_geometry.angle += 90;
|
|
while (_geometry.angle >= 360) {
|
|
_geometry.angle -= 360;
|
|
}
|
|
applyGeometry();
|
|
invalidateCoverPreview();
|
|
});
|
|
_flip->setClickedCallback([=] {
|
|
_geometry.flipped = !_geometry.flipped;
|
|
applyGeometry();
|
|
invalidateCoverPreview();
|
|
});
|
|
_cancelButton->setClickedCallback([=] {
|
|
_cancel.fire({});
|
|
});
|
|
_confirm->setClickedCallback([=] {
|
|
_done.fire(collect());
|
|
});
|
|
}
|
|
|
|
void VideoEditor::setupStreaming() {
|
|
using namespace Media::Streaming;
|
|
|
|
auto loader = MakeFileLoader(_path);
|
|
if (!loader) {
|
|
return;
|
|
}
|
|
_instance = std::make_unique<Instance>(
|
|
std::make_shared<Document>(std::move(loader)),
|
|
nullptr);
|
|
if (!_instance->valid()) {
|
|
_instance = nullptr;
|
|
return;
|
|
}
|
|
_instance->lockPlayer();
|
|
_instance->player().updates(
|
|
) | rpl::on_next_error([=](Update &&update) {
|
|
handleUpdate(std::move(update));
|
|
}, [=](Error &&) {
|
|
_instance = nullptr;
|
|
update();
|
|
}, _instance->lifetime());
|
|
|
|
restart(_from);
|
|
}
|
|
|
|
bool VideoEditor::held() const {
|
|
return _dragging || _userPaused;
|
|
}
|
|
|
|
void VideoEditor::setupTapToPause() {
|
|
struct State {
|
|
std::optional<QPoint> pressed;
|
|
bool moved = false;
|
|
};
|
|
const auto state = lifetime().make_state<State>();
|
|
_crop->events(
|
|
) | rpl::on_next([=](not_null<QEvent*> e) {
|
|
const auto type = e->type();
|
|
if (type == QEvent::MouseButtonPress) {
|
|
const auto mouse = static_cast<QMouseEvent*>(e.get());
|
|
if (mouse->button() == Qt::LeftButton) {
|
|
state->pressed = mouse->pos();
|
|
state->moved = false;
|
|
}
|
|
} else if (type == QEvent::MouseMove) {
|
|
if (state->pressed.has_value() && !state->moved) {
|
|
const auto mouse = static_cast<QMouseEvent*>(e.get());
|
|
const auto shift = mouse->pos() - *state->pressed;
|
|
if (shift.manhattanLength()
|
|
>= QApplication::startDragDistance()) {
|
|
state->moved = true;
|
|
}
|
|
}
|
|
} else if (type == QEvent::MouseButtonRelease) {
|
|
const auto tapped = state->pressed.has_value() && !state->moved;
|
|
const auto dragged = state->pressed.has_value() && state->moved;
|
|
state->pressed = std::nullopt;
|
|
state->moved = false;
|
|
if (tapped) {
|
|
togglePause();
|
|
} else if (dragged) {
|
|
invalidateCoverPreview();
|
|
}
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
void VideoEditor::togglePause() {
|
|
if (!_instance) {
|
|
return;
|
|
}
|
|
_userPaused = !_userPaused;
|
|
if (_userPaused) {
|
|
_instance->pause();
|
|
} else {
|
|
_instance->resume();
|
|
}
|
|
update();
|
|
}
|
|
|
|
void VideoEditor::invalidateCoverPreview() {
|
|
_bubbleCover = -1;
|
|
refreshCoverPreview();
|
|
}
|
|
|
|
void VideoEditor::refreshCoverPreview() {
|
|
if (_bubbleBusy || _bubbleCover == _cover) {
|
|
return;
|
|
}
|
|
_bubbleCover = _cover;
|
|
_bubbleBusy = true;
|
|
|
|
const auto cover = _cover;
|
|
const auto path = _path;
|
|
const auto dimensions = _dimensions;
|
|
const auto side = st::videoEditorBubbleSize
|
|
* style::DevicePixelRatio()
|
|
* 2;
|
|
auto mods = VideoModifications{
|
|
.geometry = _geometry,
|
|
.cover = _cover,
|
|
};
|
|
mods.geometry.crop = _crop->saveCropRect();
|
|
|
|
crl::async([=, weak = base::make_weak(this)] {
|
|
auto preview = ExtractCoverImage(path, mods, dimensions, side);
|
|
crl::on_main(weak, [=, preview = std::move(preview)]() mutable {
|
|
_bubbleBusy = false;
|
|
if (!preview.isNull() && cover == _bubbleCover) {
|
|
_bubblePreview = std::move(preview);
|
|
updateBubble();
|
|
}
|
|
refreshCoverPreview();
|
|
});
|
|
});
|
|
}
|
|
|
|
QRect VideoEditor::captureFrom() const {
|
|
if (!_crop || _innerRect.isEmpty()) {
|
|
return {};
|
|
}
|
|
return _crop->paintRect().translated(_crop->pos());
|
|
}
|
|
|
|
QSize VideoEditor::bubbleSize() const {
|
|
const auto side = st::videoEditorBubbleSize;
|
|
const auto preview = _bubblePreview.size();
|
|
if (preview.isEmpty()) {
|
|
return QSize(side, side);
|
|
}
|
|
const auto scaled = preview.scaled(side, side, Qt::KeepAspectRatio);
|
|
return QSize(
|
|
std::max(scaled.width(), 1),
|
|
std::max(scaled.height(), 1));
|
|
}
|
|
|
|
void VideoEditor::startCapture() {
|
|
if (!_capture || _bubblePreview.isNull() || captureFrom().isEmpty()) {
|
|
return;
|
|
}
|
|
_bubbleShown.stop();
|
|
_bubbleVisible = false;
|
|
_bubble->hide();
|
|
|
|
_capture->setGeometry(rect());
|
|
_capture->show();
|
|
_capture->raise();
|
|
_captureShown.start([=] {
|
|
_capture->update();
|
|
if (!_captureShown.animating()) {
|
|
_capture->hide();
|
|
_bubbleVisible = true;
|
|
_bubbleShownAt = crl::now();
|
|
_bubble->show();
|
|
_bubble->raise();
|
|
_bubble->update();
|
|
_bubbleHideTimer.callOnce(kBubbleMinVisible);
|
|
}
|
|
}, 0., 1., kCaptureDuration, anim::easeOutQuint);
|
|
_capture->update();
|
|
}
|
|
|
|
void VideoEditor::updateBubble() {
|
|
if (!_bubble || !_timeline) {
|
|
return;
|
|
}
|
|
auto visible = _dragging
|
|
&& _timeline->draggingHead()
|
|
&& !_bubblePreview.isNull();
|
|
if (_captureShown.animating()) {
|
|
if (!visible) {
|
|
return; // Let the flight finish on its own.
|
|
}
|
|
_captureShown.stop();
|
|
_capture->hide();
|
|
}
|
|
if (!visible && _bubbleVisible) {
|
|
const auto shownFor = crl::now() - _bubbleShownAt;
|
|
if (shownFor < kBubbleMinVisible) {
|
|
if (!_bubbleHideTimer.isActive()) {
|
|
_bubbleHideTimer.callOnce(kBubbleMinVisible - shownFor);
|
|
}
|
|
visible = true;
|
|
}
|
|
}
|
|
if (_bubbleVisible != visible) {
|
|
_bubbleVisible = visible;
|
|
if (visible) {
|
|
_bubbleShownAt = crl::now();
|
|
_bubbleHideTimer.cancel();
|
|
}
|
|
_bubbleShown.start(
|
|
[=] {
|
|
_bubble->update();
|
|
if (!_bubbleVisible && !_bubbleShown.animating()) {
|
|
_bubble->hide();
|
|
}
|
|
},
|
|
visible ? 0. : 1.,
|
|
visible ? 1. : 0.,
|
|
kBubbleDuration,
|
|
anim::easeOutQuint);
|
|
}
|
|
if (!visible && !_bubbleShown.animating()) {
|
|
_bubble->hide();
|
|
return;
|
|
} else if (_bubblePreview.isNull()) {
|
|
return;
|
|
}
|
|
const auto size = bubbleSize();
|
|
const auto dot = _timeline->mapTo(this, _timeline->coverDot());
|
|
const auto top = _timeline->mapTo(this, QPoint()).y()
|
|
- st::videoEditorBubbleSkip
|
|
- size.height();
|
|
_bubble->setGeometry(
|
|
std::clamp(
|
|
dot.x() - size.width() / 2,
|
|
0,
|
|
std::max(width() - size.width(), 0)),
|
|
top,
|
|
size.width(),
|
|
std::max(dot.y() - top, size.height()));
|
|
_bubble->show();
|
|
_bubble->raise();
|
|
_bubble->update();
|
|
}
|
|
|
|
void VideoEditor::restart(crl::time position) {
|
|
if (!_instance) {
|
|
return;
|
|
}
|
|
using namespace Media::Streaming;
|
|
if (!_frameRect.isEmpty()
|
|
&& _instance->player().ready()
|
|
&& !_instance->player().videoSize().isEmpty()) {
|
|
_lastFrame = _instance->frame(
|
|
FrameRequestFor(_frameRect.size())).copy();
|
|
}
|
|
_position = std::clamp(position, _from, _till);
|
|
auto options = PlaybackOptions();
|
|
options.mode = Mode::Video;
|
|
options.position = _position;
|
|
options.loop = false;
|
|
_instance->play(options);
|
|
if (held()) {
|
|
_instance->pause();
|
|
}
|
|
if (!_dragging && _timeline) {
|
|
_timeline->setPlaybackPosition(_position);
|
|
}
|
|
update();
|
|
}
|
|
|
|
void VideoEditor::handleUpdate(Media::Streaming::Update &&update) {
|
|
using namespace Media::Streaming;
|
|
v::match(update.data, [&](Information &) {
|
|
this->update();
|
|
}, [&](PreloadedVideo) {
|
|
}, [&](UpdateVideo &data) {
|
|
_position = data.position;
|
|
if (held()) {
|
|
_instance->pause();
|
|
this->update();
|
|
updateBubble();
|
|
return;
|
|
}
|
|
if (_position >= _till) {
|
|
restart(_from);
|
|
return;
|
|
}
|
|
if (_timeline) {
|
|
_timeline->setPlaybackPosition(_position);
|
|
}
|
|
this->update();
|
|
}, [&](PreloadedAudio) {
|
|
}, [&](UpdateAudio) {
|
|
}, [&](WaitingForData) {
|
|
}, [&](SpeedEstimate) {
|
|
}, [&](MutedByOther) {
|
|
}, [&](Finished) {
|
|
restart(_from);
|
|
});
|
|
}
|
|
|
|
void VideoEditor::applyGeometry() {
|
|
const auto size = this->size();
|
|
if (size.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto controlsHeight = st::videoEditorControlsHeight;
|
|
const auto contentRect = rect() - st::videoEditorContentMargins;
|
|
if (contentRect.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto frameSizeF = [&] {
|
|
const auto rotated = FlipSizeByRotation(
|
|
contentRect.size(),
|
|
_geometry.angle);
|
|
const auto m = _crop->cropMargins();
|
|
const auto forCrop = rotated
|
|
- QSize(m.left() + m.right(), m.top() + m.bottom());
|
|
const auto original = QSizeF(_dimensions);
|
|
if ((original.width() > forCrop.width())
|
|
|| (original.height() > forCrop.height())) {
|
|
return original.scaled(forCrop, Qt::KeepAspectRatio);
|
|
}
|
|
return original;
|
|
}();
|
|
const auto frameSize = QSize(
|
|
int(frameSizeF.width()),
|
|
int(frameSizeF.height()));
|
|
_frameRect = QRect(
|
|
QPoint(-frameSize.width() / 2, -frameSize.height() / 2),
|
|
frameSize);
|
|
|
|
_frameMatrix.reset();
|
|
_frameMatrix.translate(
|
|
contentRect.x() + contentRect.width() / 2,
|
|
contentRect.y() + contentRect.height() / 2);
|
|
if (_geometry.flipped) {
|
|
_frameMatrix.scale(-1, 1);
|
|
}
|
|
_frameMatrix.rotate(_geometry.angle);
|
|
|
|
const auto geometry = _frameMatrix.mapRect(_frameRect);
|
|
_crop->applyTransform(
|
|
geometry + _crop->cropMargins(),
|
|
_geometry.angle,
|
|
_geometry.flipped,
|
|
frameSizeF);
|
|
_crop->setCornersLevel(_geometry.cornersLevel);
|
|
_innerRect = geometry;
|
|
|
|
const auto controlsTop = size.height() - controlsHeight;
|
|
_controls->setGeometry(0, controlsTop, size.width(), controlsHeight);
|
|
|
|
const auto available = std::max(
|
|
size.width() - st::videoEditorContentMargins.left() * 2,
|
|
1);
|
|
const auto barWidth = std::min(
|
|
int(st::photoEditorButtonBarWidth),
|
|
available);
|
|
const auto barLeft = (size.width() - barWidth) / 2;
|
|
|
|
_timeline->resizeToWidth(barWidth);
|
|
_timeline->move(barLeft, st::videoEditorTimelineTop);
|
|
|
|
auto below = _timeline->y() + _timeline->height();
|
|
if (_hint) {
|
|
_hint->resizeToWidth(barWidth);
|
|
_hint->move(barLeft, below + st::videoEditorHintSkip);
|
|
below = _hint->y() + _hint->height();
|
|
}
|
|
_bar->setGeometry(
|
|
barLeft,
|
|
below + st::videoEditorBarSkip,
|
|
barWidth,
|
|
st::photoEditorButtonBarHeight);
|
|
static_cast<ControlsBar*>(_bar.get())->layoutChildren();
|
|
updateBubble();
|
|
|
|
if (_about) {
|
|
const auto margin = st::videoEditorAboutMargin;
|
|
_about->resizeToWidth(size.width() - margin.left() - margin.right());
|
|
_about->move(margin.left(), margin.top());
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void VideoEditor::paint(QPainter &p) {
|
|
if (_frameRect.isEmpty()) {
|
|
return;
|
|
}
|
|
auto frame = QImage();
|
|
if (_instance
|
|
&& _instance->player().ready()
|
|
&& !_instance->player().videoSize().isEmpty()) {
|
|
frame = _instance->frame(FrameRequestFor(_frameRect.size()));
|
|
if (!held()) {
|
|
// Marking a frame shown lets the player walk past a pause.
|
|
_instance->markFrameShown();
|
|
}
|
|
}
|
|
if (frame.isNull()) {
|
|
frame = _lastFrame;
|
|
}
|
|
if (frame.isNull()) {
|
|
return;
|
|
}
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setTransform(_frameMatrix);
|
|
p.drawImage(_frameRect, frame);
|
|
p.resetTransform();
|
|
|
|
if (_userPaused) {
|
|
paintPlayBadge(p);
|
|
}
|
|
}
|
|
|
|
void VideoEditor::paintPlayBadge(QPainter &p) {
|
|
const auto side = st::videoEditorPlayBadgeSize;
|
|
const auto center = _innerRect.isEmpty()
|
|
? rect().center()
|
|
: _innerRect.center();
|
|
const auto badge = QRect(
|
|
center.x() - side / 2,
|
|
center.y() - side / 2,
|
|
side,
|
|
side);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(st::videoEditorPlayBadgeBg);
|
|
p.drawEllipse(badge);
|
|
const auto &icon = st::videoEditorPlayIcon;
|
|
icon.paintInCenter(p, badge);
|
|
}
|
|
|
|
void VideoEditor::keyPressEvent(QKeyEvent *e) {
|
|
if (e->key() == Qt::Key_Escape) {
|
|
_cancel.fire({});
|
|
} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
|
_done.fire(collect());
|
|
}
|
|
}
|
|
|
|
VideoModifications VideoEditor::collect() const {
|
|
auto geometry = _geometry;
|
|
geometry.crop = _crop->saveCropRect();
|
|
return {
|
|
.geometry = std::move(geometry),
|
|
.from = _timeline->from(),
|
|
.till = _timeline->till(),
|
|
.cover = _timeline->cover(),
|
|
};
|
|
}
|
|
|
|
void InitVideoEditorLayer(
|
|
not_null<Ui::LayerWidget*> layer,
|
|
not_null<VideoEditor*> editor,
|
|
Fn<void(VideoModifications)> doneCallback) {
|
|
editor->cancelRequests(
|
|
) | rpl::on_next([=] {
|
|
layer->closeLayer();
|
|
}, editor->lifetime());
|
|
|
|
const auto weak = base::make_weak(layer.get());
|
|
editor->doneRequests(
|
|
) | rpl::on_next([=, done = std::move(doneCallback)](
|
|
const VideoModifications &mods) {
|
|
done(mods);
|
|
if (const auto strong = weak.get()) {
|
|
strong->closeLayer();
|
|
}
|
|
}, editor->lifetime());
|
|
}
|
|
|
|
} // namespace Editor
|