1805 lines
50 KiB
C++
1805 lines
50 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 "media/view/media_view_pip.h"
|
|
|
|
#include "media/streaming/media_streaming_player.h"
|
|
#include "media/streaming/media_streaming_document.h"
|
|
#include "media/streaming/media_streaming_utility.h"
|
|
#include "media/view/media_view_playback_progress.h"
|
|
#include "media/view/media_view_pip_opengl.h"
|
|
#include "media/view/media_view_pip_raster.h"
|
|
#include "media/audio/media_audio.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_media_rotation.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_session.h"
|
|
#include "core/application.h"
|
|
#include "base/platform/base_platform_info.h"
|
|
#include "base/power_save_blocker.h"
|
|
#include "base/event_filter.h"
|
|
#include "ui/platform/ui_platform_utility.h"
|
|
#include "ui/platform/ui_platform_window_title.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/wrap/fade_wrap.h"
|
|
#include "ui/widgets/shadow.h"
|
|
#include "ui/text/format_values.h"
|
|
#include "ui/gl/gl_surface.h"
|
|
#include "ui/painter.h"
|
|
#include "window/window_controller.h"
|
|
#include "styles/style_widgets.h"
|
|
#include "styles/style_window.h"
|
|
#include "styles/style_media_view.h"
|
|
|
|
#include <QtGui/QWindow>
|
|
#include <QtGui/QScreen>
|
|
#include <QtWidgets/QApplication>
|
|
|
|
namespace Media {
|
|
namespace View {
|
|
namespace {
|
|
|
|
constexpr auto kPipLoaderPriority = 2;
|
|
constexpr auto kMsInSecond = 1000;
|
|
|
|
[[nodiscard]] QRect ScreenFromPosition(QPoint point) {
|
|
const auto screen = QGuiApplication::screenAt(point);
|
|
const auto use = screen ? screen : QGuiApplication::primaryScreen();
|
|
return use
|
|
? use->availableGeometry()
|
|
: QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight);
|
|
}
|
|
|
|
[[nodiscard]] QSize MaxAllowedSizeForScreen(QSize screenSize) {
|
|
// Each side should be less than screen side - 3 * st::pipBorderSkip,
|
|
// That way it won't try to snap to both opposite sides of the screen.
|
|
const auto skip = 3 * st::pipBorderSkip;
|
|
return { screenSize.width() - skip, screenSize.height() - skip };
|
|
}
|
|
|
|
[[nodiscard]] QPoint ClampToEdges(QRect screen, QRect inner) {
|
|
const auto skip = st::pipBorderSkip;
|
|
const auto area = st::pipBorderSnapArea;
|
|
const auto sleft = screen.x() + skip;
|
|
const auto stop = screen.y() + skip;
|
|
const auto sright = screen.x() + screen.width() - skip;
|
|
const auto sbottom = screen.y() + screen.height() - skip;
|
|
const auto ileft = inner.x();
|
|
const auto itop = inner.y();
|
|
const auto iright = inner.x() + inner.width();
|
|
const auto ibottom = inner.y() + inner.height();
|
|
auto shiftx = 0;
|
|
auto shifty = 0;
|
|
if (iright + shiftx >= sright - area && iright + shiftx < sright + area) {
|
|
shiftx += (sright - iright);
|
|
}
|
|
if (ileft + shiftx >= sleft - area && ileft + shiftx < sleft + area) {
|
|
shiftx += (sleft - ileft);
|
|
}
|
|
if (ibottom + shifty >= sbottom - area && ibottom + shifty < sbottom + area) {
|
|
shifty += (sbottom - ibottom);
|
|
}
|
|
if (itop + shifty >= stop - area && itop + shifty < stop + area) {
|
|
shifty += (stop - itop);
|
|
}
|
|
return inner.topLeft() + QPoint(shiftx, shifty);
|
|
}
|
|
|
|
[[nodiscard]] QRect Transformed(
|
|
QRect original,
|
|
QSize minimalSize,
|
|
QSize maximalSize,
|
|
QPoint delta,
|
|
RectPart by) {
|
|
const auto width = original.width();
|
|
const auto height = original.height();
|
|
const auto x1 = width - minimalSize.width();
|
|
const auto x2 = maximalSize.width() - width;
|
|
const auto y1 = height - minimalSize.height();
|
|
const auto y2 = maximalSize.height() - height;
|
|
switch (by) {
|
|
case RectPart::Center: return original.translated(delta);
|
|
case RectPart::TopLeft:
|
|
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
|
|
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
|
|
return original;
|
|
case RectPart::TopRight:
|
|
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
|
|
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
|
|
return original;
|
|
case RectPart::BottomRight:
|
|
original.setHeight(
|
|
original.height() + std::clamp(delta.y(), -y1, y2));
|
|
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
|
|
return original;
|
|
case RectPart::BottomLeft:
|
|
original.setHeight(
|
|
original.height() + std::clamp(delta.y(), -y1, y2));
|
|
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
|
|
return original;
|
|
case RectPart::Left:
|
|
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
|
|
return original;
|
|
case RectPart::Top:
|
|
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
|
|
return original;
|
|
case RectPart::Right:
|
|
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
|
|
return original;
|
|
case RectPart::Bottom:
|
|
original.setHeight(
|
|
original.height() + std::clamp(delta.y(), -y1, y2));
|
|
return original;
|
|
}
|
|
return original;
|
|
Unexpected("RectPart in PiP Transformed.");
|
|
}
|
|
|
|
[[nodiscard]] QRect Constrained(
|
|
QRect original,
|
|
QSize minimalSize,
|
|
QSize maximalSize,
|
|
QSize ratio,
|
|
RectPart by,
|
|
RectParts attached) {
|
|
if (by == RectPart::Center) {
|
|
return original;
|
|
} else if (!original.width() && !original.height()) {
|
|
return QRect(original.topLeft(), ratio);
|
|
}
|
|
const auto widthLarger = (original.width() * ratio.height())
|
|
> (original.height() * ratio.width());
|
|
const auto desiredSize = ratio.scaled(
|
|
original.size(),
|
|
(((RectParts(by) & RectPart::AllCorners)
|
|
|| ((by == RectPart::Top || by == RectPart::Bottom)
|
|
&& widthLarger)
|
|
|| ((by == RectPart::Left || by == RectPart::Right)
|
|
&& !widthLarger))
|
|
? Qt::KeepAspectRatio
|
|
: Qt::KeepAspectRatioByExpanding));
|
|
const auto newSize = QSize(
|
|
std::clamp(
|
|
desiredSize.width(),
|
|
minimalSize.width(),
|
|
maximalSize.width()),
|
|
std::clamp(
|
|
desiredSize.height(),
|
|
minimalSize.height(),
|
|
maximalSize.height()));
|
|
switch (by) {
|
|
case RectPart::TopLeft:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
original.width() - newSize.width(),
|
|
original.height() - newSize.height()),
|
|
newSize);
|
|
case RectPart::TopRight:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
0,
|
|
original.height() - newSize.height()),
|
|
newSize);
|
|
case RectPart::BottomRight:
|
|
return QRect(original.topLeft(), newSize);
|
|
case RectPart::BottomLeft:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
original.width() - newSize.width(),
|
|
0),
|
|
newSize);
|
|
case RectPart::Left:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
(original.width() - newSize.width()),
|
|
((attached & RectPart::Top)
|
|
? 0
|
|
: (attached & RectPart::Bottom)
|
|
? (original.height() - newSize.height())
|
|
: (original.height() - newSize.height()) / 2)),
|
|
newSize);
|
|
case RectPart::Top:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
((attached & RectPart::Left)
|
|
? 0
|
|
: (attached & RectPart::Right)
|
|
? (original.width() - newSize.width())
|
|
: (original.width() - newSize.width()) / 2),
|
|
0),
|
|
newSize);
|
|
case RectPart::Right:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
0,
|
|
((attached & RectPart::Top)
|
|
? 0
|
|
: (attached & RectPart::Bottom)
|
|
? (original.height() - newSize.height())
|
|
: (original.height() - newSize.height()) / 2)),
|
|
newSize);
|
|
case RectPart::Bottom:
|
|
return QRect(
|
|
original.topLeft() + QPoint(
|
|
((attached & RectPart::Left)
|
|
? 0
|
|
: (attached & RectPart::Right)
|
|
? (original.width() - newSize.width())
|
|
: (original.width() - newSize.width()) / 2),
|
|
(original.height() - newSize.height())),
|
|
newSize);
|
|
}
|
|
Unexpected("RectPart in PiP Constrained.");
|
|
}
|
|
|
|
[[nodiscard]] QByteArray Serialize(const PipPanel::Position &position) {
|
|
auto result = QByteArray();
|
|
auto stream = QDataStream(&result, QIODevice::WriteOnly);
|
|
stream.setVersion(QDataStream::Qt_5_3);
|
|
stream
|
|
<< qint32(position.attached.value())
|
|
<< qint32(position.snapped.value())
|
|
<< position.screen
|
|
<< position.geometry;
|
|
stream.device()->close();
|
|
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] PipPanel::Position Deserialize(const QByteArray &data) {
|
|
auto stream = QDataStream(data);
|
|
auto result = PipPanel::Position();
|
|
auto attached = qint32(0);
|
|
auto snapped = qint32(0);
|
|
stream >> attached >> snapped >> result.screen >> result.geometry;
|
|
if (stream.status() != QDataStream::Ok) {
|
|
return {};
|
|
}
|
|
result.attached = RectParts::from_raw(attached);
|
|
result.snapped = RectParts::from_raw(snapped);
|
|
return result;
|
|
}
|
|
|
|
Qt::Edges RectPartToQtEdges(RectPart rectPart) {
|
|
switch (rectPart) {
|
|
case RectPart::TopLeft:
|
|
return Qt::TopEdge | Qt::LeftEdge;
|
|
case RectPart::TopRight:
|
|
return Qt::TopEdge | Qt::RightEdge;
|
|
case RectPart::BottomRight:
|
|
return Qt::BottomEdge | Qt::RightEdge;
|
|
case RectPart::BottomLeft:
|
|
return Qt::BottomEdge | Qt::LeftEdge;
|
|
case RectPart::Left:
|
|
return Qt::LeftEdge;
|
|
case RectPart::Top:
|
|
return Qt::TopEdge;
|
|
case RectPart::Right:
|
|
return Qt::RightEdge;
|
|
case RectPart::Bottom:
|
|
return Qt::BottomEdge;
|
|
}
|
|
|
|
return Qt::Edges();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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.");
|
|
}
|
|
|
|
bool UsePainterRotation(int rotation) {
|
|
return !(rotation % 180);
|
|
}
|
|
|
|
QSize FlipSizeByRotation(QSize size, int rotation) {
|
|
return (((rotation / 90) % 2) == 1)
|
|
? QSize(size.height(), size.width())
|
|
: size;
|
|
}
|
|
|
|
QImage RotateFrameImage(QImage image, int rotation) {
|
|
auto transform = QTransform();
|
|
transform.rotate(rotation);
|
|
return image.transformed(transform);
|
|
}
|
|
|
|
PipPanel::PipPanel(
|
|
QWidget *parent,
|
|
Fn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer)
|
|
: _content(Ui::GL::CreateSurface(std::move(renderer)))
|
|
, _parent(parent) {
|
|
}
|
|
|
|
void PipPanel::init() {
|
|
widget()->setWindowFlags(Qt::Tool
|
|
| Qt::WindowStaysOnTopHint
|
|
| Qt::FramelessWindowHint
|
|
| Qt::WindowDoesNotAcceptFocus);
|
|
widget()->setAttribute(Qt::WA_ShowWithoutActivating);
|
|
widget()->setAttribute(Qt::WA_MacAlwaysShowToolWindow);
|
|
widget()->setAttribute(Qt::WA_NoSystemBackground);
|
|
widget()->setAttribute(Qt::WA_TranslucentBackground);
|
|
Ui::Platform::IgnoreAllActivation(widget());
|
|
Ui::Platform::InitOnTopPanel(widget());
|
|
widget()->setMouseTracking(true);
|
|
widget()->resize(0, 0);
|
|
widget()->hide();
|
|
widget()->createWinId();
|
|
|
|
rp()->shownValue(
|
|
) | rpl::filter([=](bool shown) {
|
|
return shown;
|
|
}) | rpl::start_with_next([=] {
|
|
// Workaround Qt's forced transient parent.
|
|
Ui::Platform::ClearTransientParent(widget());
|
|
}, rp()->lifetime());
|
|
|
|
QObject::connect(
|
|
widget()->windowHandle(),
|
|
&QWindow::screenChanged,
|
|
[=](QScreen *screen) {
|
|
handleScreenChanged(screen);
|
|
});
|
|
|
|
if (Platform::IsWayland()) {
|
|
rp()->sizeValue(
|
|
) | rpl::start_with_next([=](QSize size) {
|
|
handleWaylandResize(size);
|
|
}, rp()->lifetime());
|
|
|
|
base::install_event_filter(widget(), [=](not_null<QEvent*> event) {
|
|
if (event->type() == QEvent::Resize && _inHandleWaylandResize) {
|
|
return base::EventFilterResult::Cancel;
|
|
}
|
|
return base::EventFilterResult::Continue;
|
|
});
|
|
|
|
base::install_event_filter(widget()->windowHandle(), [=](not_null<QEvent*> event) {
|
|
if (event->type() == QEvent::Resize) {
|
|
if (_inHandleWaylandResize) {
|
|
return base::EventFilterResult::Cancel;
|
|
}
|
|
const auto newSize = static_cast<QResizeEvent*>(event.get())->size();
|
|
if (_suggestedWaylandSize == newSize) {
|
|
handleWaylandResize(newSize);
|
|
return base::EventFilterResult::Cancel;
|
|
}
|
|
}
|
|
return base::EventFilterResult::Continue;
|
|
});
|
|
}
|
|
}
|
|
|
|
not_null<QWidget*> PipPanel::widget() const {
|
|
return _content->rpWidget();
|
|
}
|
|
|
|
not_null<Ui::RpWidgetWrap*> PipPanel::rp() const {
|
|
return _content.get();
|
|
}
|
|
|
|
void PipPanel::setAspectRatio(QSize ratio) {
|
|
if (_ratio == ratio) {
|
|
return;
|
|
}
|
|
_ratio = ratio;
|
|
if (_ratio.isEmpty()) {
|
|
_ratio = QSize(1, 1);
|
|
}
|
|
Ui::Platform::DisableSystemWindowResize(widget(), _ratio);
|
|
if (!widget()->size().isEmpty()) {
|
|
setPosition(countPosition());
|
|
}
|
|
}
|
|
|
|
void PipPanel::setPosition(Position position) {
|
|
if (!position.screen.isEmpty()) {
|
|
for (const auto screen : QApplication::screens()) {
|
|
if (screen->geometry() == position.screen) {
|
|
setPositionOnScreen(position, screen->availableGeometry());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
setPositionDefault();
|
|
}
|
|
|
|
QRect PipPanel::inner() const {
|
|
return widget()->rect().marginsRemoved(_padding);
|
|
}
|
|
|
|
RectParts PipPanel::attached() const {
|
|
return _attached;
|
|
}
|
|
|
|
bool PipPanel::useTransparency() const {
|
|
return _useTransparency;
|
|
}
|
|
|
|
void PipPanel::setDragDisabled(bool disabled) {
|
|
_dragDisabled = disabled;
|
|
if (_dragState) {
|
|
_dragState = std::nullopt;
|
|
}
|
|
}
|
|
|
|
bool PipPanel::dragging() const {
|
|
return _dragState.has_value();
|
|
}
|
|
|
|
rpl::producer<> PipPanel::saveGeometryRequests() const {
|
|
return _saveGeometryRequests.events();
|
|
}
|
|
|
|
QScreen *PipPanel::myScreen() const {
|
|
return widget()->screen();
|
|
}
|
|
|
|
PipPanel::Position PipPanel::countPosition() const {
|
|
const auto screen = myScreen();
|
|
if (!screen) {
|
|
return Position();
|
|
}
|
|
auto result = Position();
|
|
result.screen = screen->geometry();
|
|
result.geometry = widget()->geometry().marginsRemoved(_padding);
|
|
const auto available = screen->availableGeometry();
|
|
const auto skip = st::pipBorderSkip;
|
|
const auto left = result.geometry.x();
|
|
const auto right = left + result.geometry.width();
|
|
const auto top = result.geometry.y();
|
|
const auto bottom = top + result.geometry.height();
|
|
if ((!_dragState || *_dragState != RectPart::Center)
|
|
&& !Platform::IsWayland()) {
|
|
if (left == available.x()) {
|
|
result.attached |= RectPart::Left;
|
|
} else if (right == available.x() + available.width()) {
|
|
result.attached |= RectPart::Right;
|
|
} else if (left == available.x() + skip) {
|
|
result.snapped |= RectPart::Left;
|
|
} else if (right == available.x() + available.width() - skip) {
|
|
result.snapped |= RectPart::Right;
|
|
}
|
|
if (top == available.y()) {
|
|
result.attached |= RectPart::Top;
|
|
} else if (bottom == available.y() + available.height()) {
|
|
result.attached |= RectPart::Bottom;
|
|
} else if (top == available.y() + skip) {
|
|
result.snapped |= RectPart::Top;
|
|
} else if (bottom == available.y() + available.height() - skip) {
|
|
result.snapped |= RectPart::Bottom;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void PipPanel::setPositionDefault() {
|
|
const auto widgetScreen = [&](auto &&widget) -> QScreen* {
|
|
if (!widget) {
|
|
return nullptr;
|
|
}
|
|
if (!Platform::IsWayland()) {
|
|
if (const auto screen = QGuiApplication::screenAt(
|
|
widget->geometry().center())) {
|
|
return screen;
|
|
}
|
|
}
|
|
return widget->screen();
|
|
};
|
|
const auto parentScreen = widgetScreen(_parent);
|
|
const auto myScreen = widget()->screen();
|
|
if (parentScreen && myScreen != parentScreen) {
|
|
widget()->windowHandle()->setScreen(parentScreen);
|
|
}
|
|
auto position = Position();
|
|
position.snapped = RectPart::Top | RectPart::Left;
|
|
position.screen = parentScreen->geometry();
|
|
position.geometry = QRect(0, 0, st::pipDefaultSize, st::pipDefaultSize);
|
|
setPositionOnScreen(position, parentScreen->availableGeometry());
|
|
}
|
|
|
|
void PipPanel::setPositionOnScreen(Position position, QRect available) {
|
|
const auto screen = available;
|
|
const auto requestedSize = position.geometry.size();
|
|
const auto max = std::max(requestedSize.width(), requestedSize.height());
|
|
|
|
// Apply aspect ratio.
|
|
const auto scaled = (_ratio.width() > _ratio.height())
|
|
? QSize(max, max * _ratio.height() / _ratio.width())
|
|
: QSize(max * _ratio.width() / _ratio.height(), max);
|
|
|
|
// Apply maximum size.
|
|
const auto fit = MaxAllowedSizeForScreen(screen.size());
|
|
const auto byWidth = (scaled.width() * fit.height())
|
|
> (scaled.height() * fit.width());
|
|
const auto normalized = (byWidth && scaled.width() > fit.width())
|
|
? QSize(fit.width(), fit.width() * scaled.height() / scaled.width())
|
|
: (!byWidth && scaled.height() > fit.height())
|
|
? QSize(
|
|
fit.height() * scaled.width() / scaled.height(),
|
|
fit.height())
|
|
: scaled;
|
|
|
|
// Apply minimal size.
|
|
const auto min = st::pipMinimalSize;
|
|
const auto minimalSize = (_ratio.width() > _ratio.height())
|
|
? QSize(min * _ratio.width() / _ratio.height(), min)
|
|
: QSize(min, min * _ratio.height() / _ratio.width());
|
|
const auto size = QSize(
|
|
std::max(normalized.width(), minimalSize.width()),
|
|
std::max(normalized.height(), minimalSize.height()));
|
|
|
|
// Apply maximal size.
|
|
const auto maximalSize = byWidth
|
|
? QSize(fit.width(), fit.width() * _ratio.height() / _ratio.width())
|
|
: QSize(fit.height() * _ratio.width() / _ratio.height(), fit.height());
|
|
|
|
// Apply left-right screen borders.
|
|
const auto skip = st::pipBorderSkip;
|
|
const auto inner = screen.marginsRemoved({ skip, skip, skip, skip });
|
|
auto geometry = QRect(position.geometry.topLeft(), size);
|
|
if ((position.attached & RectPart::Left)
|
|
|| (geometry.x() < screen.x())) {
|
|
geometry.moveLeft(screen.x());
|
|
} else if ((position.attached & RectPart::Right)
|
|
|| (geometry.x() + geometry.width() > screen.x() + screen.width())) {
|
|
geometry.moveLeft(screen.x() + screen.width() - geometry.width());
|
|
} else if (position.snapped & RectPart::Left) {
|
|
geometry.moveLeft(inner.x());
|
|
} else if (position.snapped & RectPart::Right) {
|
|
geometry.moveLeft(inner.x() + inner.width() - geometry.width());
|
|
}
|
|
|
|
// Apply top-bottom screen borders.
|
|
if ((position.attached & RectPart::Top) || (geometry.y() < screen.y())) {
|
|
geometry.moveTop(screen.y());
|
|
} else if ((position.attached & RectPart::Bottom)
|
|
|| (geometry.y() + geometry.height()
|
|
> screen.y() + screen.height())) {
|
|
geometry.moveTop(
|
|
screen.y() + screen.height() - geometry.height());
|
|
} else if (position.snapped & RectPart::Top) {
|
|
geometry.moveTop(inner.y());
|
|
} else if (position.snapped & RectPart::Bottom) {
|
|
geometry.moveTop(inner.y() + inner.height() - geometry.height());
|
|
}
|
|
|
|
geometry += _padding;
|
|
|
|
setGeometry(geometry);
|
|
widget()->setMinimumSize(minimalSize);
|
|
widget()->setMaximumSize(
|
|
std::max(minimalSize.width(), maximalSize.width()),
|
|
std::max(minimalSize.height(), maximalSize.height()));
|
|
updateDecorations();
|
|
}
|
|
|
|
void PipPanel::update() {
|
|
widget()->update();
|
|
}
|
|
|
|
void PipPanel::setGeometry(QRect geometry) {
|
|
widget()->setGeometry(geometry);
|
|
}
|
|
|
|
void PipPanel::handleWaylandResize(QSize size) {
|
|
_inHandleWaylandResize = true;
|
|
_suggestedWaylandSize = size;
|
|
|
|
// Apply aspect ratio.
|
|
const auto max = std::max(size.width(), size.height());
|
|
const auto scaled = (_ratio.width() > _ratio.height())
|
|
? QSize(max, max * _ratio.height() / _ratio.width())
|
|
: QSize(max * _ratio.width() / _ratio.height(), max);
|
|
|
|
// Buffer can't be bigger than the configured
|
|
// (suggested by compositor) size.
|
|
const auto byWidth = (scaled.width() * size.height())
|
|
> (scaled.height() * size.width());
|
|
const auto normalized = (byWidth && scaled.width() > size.width())
|
|
? QSize(size.width(), size.width() * scaled.height() / scaled.width())
|
|
: (!byWidth && scaled.height() > size.height())
|
|
? QSize(
|
|
size.height() * scaled.width() / scaled.height(),
|
|
size.height())
|
|
: scaled;
|
|
|
|
widget()->resize(normalized);
|
|
_inHandleWaylandResize = false;
|
|
}
|
|
|
|
void PipPanel::handleScreenChanged(QScreen *screen) {
|
|
const auto screenGeometry = screen->availableGeometry();
|
|
const auto minimalSize = _ratio.scaled(
|
|
st::pipMinimalSize,
|
|
st::pipMinimalSize,
|
|
Qt::KeepAspectRatioByExpanding);
|
|
const auto maximalSize = _ratio.scaled(
|
|
MaxAllowedSizeForScreen(screenGeometry.size()),
|
|
Qt::KeepAspectRatio);
|
|
widget()->setMinimumSize(minimalSize);
|
|
widget()->setMaximumSize(
|
|
std::max(minimalSize.width(), maximalSize.width()),
|
|
std::max(minimalSize.height(), maximalSize.height()));
|
|
}
|
|
|
|
void PipPanel::handleMousePress(QPoint position, Qt::MouseButton button) {
|
|
if (button != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
updateOverState(position);
|
|
_pressState = _overState;
|
|
_pressPoint = QCursor::pos();
|
|
}
|
|
|
|
void PipPanel::handleMouseRelease(QPoint position, Qt::MouseButton button) {
|
|
if (button != Qt::LeftButton || !base::take(_pressState)) {
|
|
return;
|
|
} else if (base::take(_dragState)) {
|
|
finishDrag(QCursor::pos());
|
|
updateOverState(position);
|
|
}
|
|
}
|
|
|
|
void PipPanel::updateOverState(QPoint point) {
|
|
const auto size = st::pipResizeArea;
|
|
const auto ignore = _attached | _snapped;
|
|
const auto count = [&](RectPart side, int padding) {
|
|
return (ignore & side) ? 0 : padding ? padding : size;
|
|
};
|
|
const auto left = count(RectPart::Left, _padding.left());
|
|
const auto top = count(RectPart::Top, _padding.top());
|
|
const auto right = count(RectPart::Right, _padding.right());
|
|
const auto bottom = count(RectPart::Bottom, _padding.bottom());
|
|
const auto width = widget()->width();
|
|
const auto height = widget()->height();
|
|
const auto overState = [&] {
|
|
if (point.x() < left) {
|
|
if (point.y() < top) {
|
|
return RectPart::TopLeft;
|
|
} else if (point.y() >= height - bottom) {
|
|
return RectPart::BottomLeft;
|
|
} else {
|
|
return RectPart::Left;
|
|
}
|
|
} else if (point.x() >= width - right) {
|
|
if (point.y() < top) {
|
|
return RectPart::TopRight;
|
|
} else if (point.y() >= height - bottom) {
|
|
return RectPart::BottomRight;
|
|
} else {
|
|
return RectPart::Right;
|
|
}
|
|
} else if (point.y() < top) {
|
|
return RectPart::Top;
|
|
} else if (point.y() >= height - bottom) {
|
|
return RectPart::Bottom;
|
|
} else {
|
|
return RectPart::Center;
|
|
}
|
|
}();
|
|
if (_overState != overState) {
|
|
_overState = overState;
|
|
widget()->setCursor([&] {
|
|
switch (_overState) {
|
|
case RectPart::Center:
|
|
return style::cur_pointer;
|
|
case RectPart::TopLeft:
|
|
case RectPart::BottomRight:
|
|
return style::cur_sizefdiag;
|
|
case RectPart::TopRight:
|
|
case RectPart::BottomLeft:
|
|
return style::cur_sizebdiag;
|
|
case RectPart::Left:
|
|
case RectPart::Right:
|
|
return style::cur_sizehor;
|
|
case RectPart::Top:
|
|
case RectPart::Bottom:
|
|
return style::cur_sizever;
|
|
}
|
|
Unexpected("State in PipPanel::updateOverState.");
|
|
}());
|
|
}
|
|
}
|
|
|
|
void PipPanel::handleMouseMove(QPoint position) {
|
|
if (!_pressState) {
|
|
updateOverState(position);
|
|
return;
|
|
}
|
|
const auto point = QCursor::pos();
|
|
const auto distance = QApplication::startDragDistance();
|
|
if (!_dragState
|
|
&& (point - _pressPoint).manhattanLength() > distance
|
|
&& !_dragDisabled) {
|
|
_dragState = _pressState;
|
|
updateDecorations();
|
|
_dragStartGeometry = widget()->geometry().marginsRemoved(_padding);
|
|
}
|
|
if (_dragState) {
|
|
if (Platform::IsWayland()) {
|
|
startSystemDrag();
|
|
} else {
|
|
processDrag(point);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PipPanel::startSystemDrag() {
|
|
Expects(_dragState.has_value());
|
|
|
|
const auto stateEdges = RectPartToQtEdges(*_dragState);
|
|
if (stateEdges) {
|
|
widget()->windowHandle()->startSystemResize(stateEdges);
|
|
} else {
|
|
widget()->windowHandle()->startSystemMove();
|
|
}
|
|
|
|
Ui::SendSynteticMouseEvent(
|
|
widget().get(),
|
|
QEvent::MouseButtonRelease,
|
|
Qt::LeftButton);
|
|
}
|
|
|
|
void PipPanel::processDrag(QPoint point) {
|
|
Expects(_dragState.has_value());
|
|
|
|
const auto dragPart = *_dragState;
|
|
const auto screen = (dragPart == RectPart::Center)
|
|
? ScreenFromPosition(point)
|
|
: myScreen()
|
|
? myScreen()->availableGeometry()
|
|
: QRect();
|
|
if (screen.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto minimalSize = _ratio.scaled(
|
|
st::pipMinimalSize,
|
|
st::pipMinimalSize,
|
|
Qt::KeepAspectRatioByExpanding);
|
|
const auto maximalSize = _ratio.scaled(
|
|
MaxAllowedSizeForScreen(screen.size()),
|
|
Qt::KeepAspectRatio);
|
|
const auto geometry = Transformed(
|
|
_dragStartGeometry,
|
|
minimalSize,
|
|
maximalSize,
|
|
point - _pressPoint,
|
|
dragPart);
|
|
const auto valid = Constrained(
|
|
geometry,
|
|
minimalSize,
|
|
maximalSize,
|
|
_ratio,
|
|
dragPart,
|
|
_attached);
|
|
const auto clamped = (dragPart == RectPart::Center)
|
|
? ClampToEdges(screen, valid)
|
|
: valid.topLeft();
|
|
widget()->setMinimumSize(minimalSize);
|
|
widget()->setMaximumSize(
|
|
std::max(minimalSize.width(), maximalSize.width()),
|
|
std::max(minimalSize.height(), maximalSize.height()));
|
|
if (clamped != valid.topLeft()) {
|
|
moveAnimated(clamped);
|
|
} else {
|
|
const auto newGeometry = valid.marginsAdded(_padding);
|
|
_positionAnimation.stop();
|
|
setGeometry(newGeometry);
|
|
}
|
|
}
|
|
|
|
void PipPanel::finishDrag(QPoint point) {
|
|
const auto screen = ScreenFromPosition(point);
|
|
const auto inner = widget()->geometry().marginsRemoved(_padding);
|
|
const auto position = widget()->pos();
|
|
const auto clamped = [&] {
|
|
auto result = position;
|
|
if (Platform::IsWayland()) {
|
|
return result;
|
|
}
|
|
if (result.x() > screen.x() + screen.width() - inner.width()) {
|
|
result.setX(screen.x() + screen.width() - inner.width());
|
|
}
|
|
if (result.x() < screen.x()) {
|
|
result.setX(screen.x());
|
|
}
|
|
if (result.y() > screen.y() + screen.height() - inner.height()) {
|
|
result.setY(screen.y() + screen.height() - inner.height());
|
|
}
|
|
if (result.y() < screen.y()) {
|
|
result.setY(screen.y());
|
|
}
|
|
return result;
|
|
}();
|
|
if (position != clamped) {
|
|
moveAnimated(clamped);
|
|
} else {
|
|
_positionAnimation.stop();
|
|
updateDecorations();
|
|
}
|
|
}
|
|
|
|
void PipPanel::updatePositionAnimated() {
|
|
const auto progress = _positionAnimation.value(1.);
|
|
if (!_positionAnimation.animating()) {
|
|
widget()->move(_positionAnimationTo
|
|
- QPoint(_padding.left(), _padding.top()));
|
|
if (!_dragState) {
|
|
updateDecorations();
|
|
}
|
|
return;
|
|
}
|
|
const auto from = QPointF(_positionAnimationFrom);
|
|
const auto to = QPointF(_positionAnimationTo);
|
|
widget()->move((from + (to - from) * progress).toPoint()
|
|
- QPoint(_padding.left(), _padding.top()));
|
|
}
|
|
|
|
void PipPanel::moveAnimated(QPoint to) {
|
|
if (_positionAnimation.animating() && _positionAnimationTo == to) {
|
|
return;
|
|
}
|
|
_positionAnimationTo = to;
|
|
_positionAnimationFrom = widget()->pos()
|
|
+ QPoint(_padding.left(), _padding.top());
|
|
_positionAnimation.stop();
|
|
_positionAnimation.start(
|
|
[=] { updatePositionAnimated(); },
|
|
0.,
|
|
1.,
|
|
st::slideWrapDuration,
|
|
anim::easeOutCirc);
|
|
}
|
|
|
|
void PipPanel::updateDecorations() {
|
|
const auto guard = gsl::finally([&] {
|
|
if (!_dragState) {
|
|
_saveGeometryRequests.fire({});
|
|
}
|
|
});
|
|
const auto position = countPosition();
|
|
const auto use = Ui::Platform::TranslucentWindowsSupported();
|
|
const auto full = use ? st::callShadow.extend : style::margins();
|
|
const auto padding = style::margins(
|
|
(position.attached & RectPart::Left) ? 0 : full.left(),
|
|
(position.attached & RectPart::Top) ? 0 : full.top(),
|
|
(position.attached & RectPart::Right) ? 0 : full.right(),
|
|
(position.attached & RectPart::Bottom) ? 0 : full.bottom());
|
|
_snapped = position.snapped;
|
|
if (_padding == padding && _attached == position.attached) {
|
|
return;
|
|
}
|
|
const auto newGeometry = position.geometry.marginsAdded(padding);
|
|
_attached = position.attached;
|
|
_padding = padding;
|
|
_useTransparency = use;
|
|
widget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
|
|
setGeometry(newGeometry);
|
|
update();
|
|
}
|
|
|
|
Pip::Pip(
|
|
not_null<Delegate*> delegate,
|
|
not_null<DocumentData*> data,
|
|
std::shared_ptr<Streaming::Document> shared,
|
|
FnMut<void()> closeAndContinue,
|
|
FnMut<void()> destroy)
|
|
: _delegate(delegate)
|
|
, _data(data)
|
|
, _instance(std::move(shared), [=] { waitingAnimationCallback(); })
|
|
, _panel(
|
|
_delegate->pipParentWidget(),
|
|
[=](Ui::GL::Capabilities capabilities) {
|
|
return chooseRenderer(capabilities);
|
|
})
|
|
, _playbackProgress(std::make_unique<PlaybackProgress>())
|
|
, _rotation(data->owner().mediaRotation().get(data))
|
|
, _lastPositiveVolume((Core::App().settings().videoVolume() > 0.)
|
|
? Core::App().settings().videoVolume()
|
|
: Core::Settings::kDefaultVolume)
|
|
, _closeAndContinue(std::move(closeAndContinue))
|
|
, _destroy(std::move(destroy)) {
|
|
setupPanel();
|
|
setupButtons();
|
|
setupStreaming();
|
|
|
|
_data->session().account().sessionChanges(
|
|
) | rpl::start_with_next([=] {
|
|
_destroy();
|
|
}, _panel.rp()->lifetime());
|
|
}
|
|
|
|
Pip::~Pip() = default;
|
|
|
|
void Pip::setupPanel() {
|
|
_panel.init();
|
|
const auto size = [&] {
|
|
if (!_instance.info().video.size.isEmpty()) {
|
|
return _instance.info().video.size;
|
|
}
|
|
const auto media = _data->activeMediaView();
|
|
if (media) {
|
|
media->goodThumbnailWanted();
|
|
}
|
|
const auto good = media ? media->goodThumbnail() : nullptr;
|
|
const auto original = good ? good->size() : _data->dimensions;
|
|
return original.isEmpty() ? QSize(1, 1) : original;
|
|
}();
|
|
_panel.setAspectRatio(FlipSizeByRotation(size, _rotation));
|
|
_panel.setPosition(Deserialize(_delegate->pipLoadGeometry()));
|
|
_panel.widget()->show();
|
|
|
|
_panel.saveGeometryRequests(
|
|
) | rpl::start_with_next([=] {
|
|
saveGeometry();
|
|
}, _panel.rp()->lifetime());
|
|
|
|
_panel.rp()->events(
|
|
) | rpl::start_with_next([=](not_null<QEvent*> e) {
|
|
const auto mousePosition = [&] {
|
|
return static_cast<QMouseEvent*>(e.get())->pos();
|
|
};
|
|
const auto mouseButton = [&] {
|
|
return static_cast<QMouseEvent*>(e.get())->button();
|
|
};
|
|
switch (e->type()) {
|
|
case QEvent::Close: handleClose(); break;
|
|
case QEvent::Leave: handleLeave(); break;
|
|
case QEvent::MouseMove:
|
|
handleMouseMove(mousePosition());
|
|
break;
|
|
case QEvent::MouseButtonPress:
|
|
handleMousePress(mousePosition(), mouseButton());
|
|
break;
|
|
case QEvent::MouseButtonRelease:
|
|
handleMouseRelease(mousePosition(), mouseButton());
|
|
break;
|
|
case QEvent::MouseButtonDblClick:
|
|
handleDoubleClick(mouseButton());
|
|
break;
|
|
}
|
|
}, _panel.rp()->lifetime());
|
|
}
|
|
|
|
void Pip::handleClose() {
|
|
crl::on_main(_panel.widget(), [=] {
|
|
_destroy();
|
|
});
|
|
}
|
|
|
|
void Pip::handleLeave() {
|
|
setOverState(OverState::None);
|
|
}
|
|
|
|
void Pip::handleMouseMove(QPoint position) {
|
|
const auto weak = Ui::MakeWeak(_panel.widget());
|
|
const auto guard = gsl::finally([&] {
|
|
if (weak) {
|
|
_panel.handleMouseMove(position);
|
|
}
|
|
});
|
|
setOverState(computeState(position));
|
|
seekUpdate(position);
|
|
volumeControllerUpdate(position);
|
|
}
|
|
|
|
void Pip::setOverState(OverState state) {
|
|
if (_over == state) {
|
|
return;
|
|
}
|
|
const auto wasShown = ResolveShownOver(_over);
|
|
_over = state;
|
|
const auto nowAreShown = (ResolveShownOver(_over) != OverState::None);
|
|
if ((wasShown != OverState::None) != nowAreShown) {
|
|
_controlsShown.start(
|
|
[=] { _panel.update(); },
|
|
nowAreShown ? 0. : 1.,
|
|
nowAreShown ? 1. : 0.,
|
|
st::fadeWrapDuration,
|
|
anim::linear);
|
|
}
|
|
if (!_pressed) {
|
|
updateActiveState(wasShown);
|
|
}
|
|
_panel.update();
|
|
}
|
|
|
|
void Pip::setPressedState(std::optional<OverState> state) {
|
|
if (_pressed == state) {
|
|
return;
|
|
}
|
|
const auto wasShown = shownActiveState();
|
|
_pressed = state;
|
|
updateActiveState(wasShown);
|
|
}
|
|
|
|
Pip::OverState Pip::shownActiveState() const {
|
|
return ResolveShownOver(_pressed.value_or(_over));
|
|
}
|
|
|
|
float64 Pip::activeValue(const Button &button) const {
|
|
const auto shownState = ResolveShownOver(button.state);
|
|
return button.active.value((shownActiveState() == shownState) ? 1. : 0.);
|
|
}
|
|
|
|
void Pip::updateActiveState(OverState wasShown) {
|
|
const auto check = [&](Button &button) {
|
|
const auto shownState = ResolveShownOver(button.state);
|
|
const auto nowIsShown = (shownActiveState() == shownState);
|
|
if ((wasShown == shownState) != nowIsShown) {
|
|
button.active.start(
|
|
[=, &button] { _panel.widget()->update(button.icon); },
|
|
nowIsShown ? 0. : 1.,
|
|
nowIsShown ? 1. : 0.,
|
|
st::fadeWrapDuration,
|
|
anim::linear);
|
|
}
|
|
};
|
|
check(_close);
|
|
check(_enlarge);
|
|
check(_play);
|
|
check(_playback);
|
|
check(_volumeToggle);
|
|
check(_volumeController);
|
|
}
|
|
|
|
Pip::OverState Pip::ResolveShownOver(OverState state) {
|
|
return (state == OverState::VolumeController)
|
|
? OverState::VolumeToggle
|
|
: state;
|
|
}
|
|
|
|
void Pip::handleMousePress(QPoint position, Qt::MouseButton button) {
|
|
const auto weak = Ui::MakeWeak(_panel.widget());
|
|
const auto guard = gsl::finally([&] {
|
|
if (weak) {
|
|
_panel.handleMousePress(position, button);
|
|
}
|
|
});
|
|
if (button != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
_pressed = _over;
|
|
if (_over == OverState::Playback || _over == OverState::VolumeController) {
|
|
_panel.setDragDisabled(true);
|
|
}
|
|
seekUpdate(position);
|
|
volumeControllerUpdate(position);
|
|
}
|
|
|
|
void Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) {
|
|
const auto weak = Ui::MakeWeak(_panel.widget());
|
|
const auto guard = gsl::finally([&] {
|
|
if (weak) {
|
|
_panel.handleMouseRelease(position, button);
|
|
}
|
|
});
|
|
if (button != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
seekUpdate(position);
|
|
|
|
volumeControllerUpdate(position);
|
|
|
|
const auto pressed = base::take(_pressed);
|
|
if (pressed && *pressed == OverState::Playback) {
|
|
_panel.setDragDisabled(false);
|
|
seekFinish(_playbackProgress->value());
|
|
} else if (pressed && *pressed == OverState::VolumeController) {
|
|
_panel.setDragDisabled(false);
|
|
_panel.update();
|
|
} else if (_panel.dragging() || !pressed || *pressed != _over) {
|
|
_lastHandledPress = std::nullopt;
|
|
} else {
|
|
_lastHandledPress = _over;
|
|
switch (_over) {
|
|
case OverState::Close: _panel.widget()->close(); break;
|
|
case OverState::Enlarge: _closeAndContinue(); break;
|
|
case OverState::VolumeToggle: volumeToggled(); break;
|
|
case OverState::Other: playbackPauseResume(); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Pip::handleDoubleClick(Qt::MouseButton button) {
|
|
if (_over != OverState::Other
|
|
|| !_lastHandledPress
|
|
|| *_lastHandledPress != _over) {
|
|
return;
|
|
}
|
|
playbackPauseResume(); // Un-click the first click.
|
|
_closeAndContinue();
|
|
}
|
|
|
|
void Pip::seekUpdate(QPoint position) {
|
|
if (!_pressed || *_pressed != OverState::Playback) {
|
|
return;
|
|
}
|
|
const auto unbound = (position.x() - _playback.icon.x())
|
|
/ float64(_playback.icon.width());
|
|
const auto progress = std::clamp(unbound, 0., 1.);
|
|
seekProgress(progress);
|
|
}
|
|
|
|
void Pip::seekProgress(float64 value) {
|
|
if (!_lastDurationMs) {
|
|
return;
|
|
}
|
|
|
|
_playbackProgress->setValue(value, false);
|
|
|
|
const auto positionMs = std::clamp(
|
|
static_cast<crl::time>(value * _lastDurationMs),
|
|
crl::time(0),
|
|
_lastDurationMs);
|
|
if (_seekPositionMs != positionMs) {
|
|
_seekPositionMs = positionMs;
|
|
if (!_instance.player().paused()
|
|
&& !_instance.player().finished()) {
|
|
_pausedBySeek = true;
|
|
playbackPauseResume();
|
|
}
|
|
updatePlaybackTexts(_seekPositionMs, _lastDurationMs, kMsInSecond);
|
|
}
|
|
}
|
|
|
|
void Pip::seekFinish(float64 value) {
|
|
if (!_lastDurationMs) {
|
|
return;
|
|
}
|
|
|
|
const auto positionMs = std::clamp(
|
|
static_cast<crl::time>(value * _lastDurationMs),
|
|
crl::time(0),
|
|
_lastDurationMs);
|
|
_seekPositionMs = -1;
|
|
_startPaused = !_pausedBySeek && !_instance.player().finished();
|
|
restartAtSeekPosition(positionMs);
|
|
}
|
|
|
|
void Pip::volumeChanged(float64 volume) {
|
|
if (volume > 0.) {
|
|
_lastPositiveVolume = volume;
|
|
}
|
|
Player::mixer()->setVideoVolume(volume);
|
|
Core::App().settings().setVideoVolume(volume);
|
|
Core::App().saveSettingsDelayed();
|
|
}
|
|
|
|
void Pip::volumeToggled() {
|
|
const auto volume = Core::App().settings().videoVolume();
|
|
volumeChanged(volume ? 0. : _lastPositiveVolume);
|
|
_panel.update();
|
|
}
|
|
|
|
void Pip::volumeControllerUpdate(QPoint position) {
|
|
if (!_pressed || *_pressed != OverState::VolumeController) {
|
|
return;
|
|
}
|
|
const auto unbound = (position.x() - _volumeController.icon.x())
|
|
/ float64(_volumeController.icon.width());
|
|
const auto value = std::clamp(unbound, 0., 1.);
|
|
volumeChanged(value);
|
|
_panel.update();
|
|
}
|
|
|
|
void Pip::setupButtons() {
|
|
_close.state = OverState::Close;
|
|
_enlarge.state = OverState::Enlarge;
|
|
_playback.state = OverState::Playback;
|
|
_volumeToggle.state = OverState::VolumeToggle;
|
|
_volumeController.state = OverState::VolumeController;
|
|
_play.state = OverState::Other;
|
|
_panel.rp()->sizeValue(
|
|
) | rpl::map([=] {
|
|
return _panel.inner();
|
|
}) | rpl::start_with_next([=](QRect rect) {
|
|
const auto skip = st::pipControlSkip;
|
|
_close.area = QRect(
|
|
rect.x(),
|
|
rect.y(),
|
|
st::pipCloseIcon.width() + 2 * skip,
|
|
st::pipCloseIcon.height() + 2 * skip);
|
|
_enlarge.area = QRect(
|
|
_close.area.x() + _close.area.width(),
|
|
rect.y(),
|
|
st::pipEnlargeIcon.width() + 2 * skip,
|
|
st::pipEnlargeIcon.height() + 2 * skip);
|
|
|
|
const auto volumeSkip = st::pipPlaybackSkip;
|
|
const auto volumeHeight = 2 * volumeSkip + st::pipPlaybackWide;
|
|
const auto volumeToggleWidth = st::pipVolumeIcon0.width()
|
|
+ 2 * skip;
|
|
const auto volumeToggleHeight = st::pipVolumeIcon0.height()
|
|
+ 2 * skip;
|
|
const auto volumeWidth = (((st::mediaviewVolumeWidth + 2 * skip)
|
|
+ _close.area.width()
|
|
+ _enlarge.area.width()
|
|
+ volumeToggleWidth) < rect.width())
|
|
? st::mediaviewVolumeWidth
|
|
: 0;
|
|
_volumeController.area = QRect(
|
|
rect.x() + rect.width() - volumeWidth - 2 * volumeSkip,
|
|
rect.y() + (volumeToggleHeight - volumeHeight) / 2,
|
|
volumeWidth,
|
|
volumeHeight);
|
|
_volumeToggle.area = QRect(
|
|
_volumeController.area.x()
|
|
- st::pipVolumeIcon0.width()
|
|
- skip,
|
|
rect.y(),
|
|
volumeToggleWidth,
|
|
volumeToggleHeight);
|
|
if (!Ui::Platform::TitleControlsOnLeft()) {
|
|
_close.area.moveLeft(rect.x()
|
|
+ rect.width()
|
|
- (_close.area.x() - rect.x())
|
|
- _close.area.width());
|
|
_enlarge.area.moveLeft(rect.x()
|
|
+ rect.width()
|
|
- (_enlarge.area.x() - rect.x())
|
|
- _enlarge.area.width());
|
|
_volumeToggle.area.moveLeft(rect.x());
|
|
_volumeController.area.moveLeft(_volumeToggle.area.x()
|
|
+ _volumeToggle.area.width());
|
|
}
|
|
_close.icon = _close.area.marginsRemoved({ skip, skip, skip, skip });
|
|
_enlarge.icon = _enlarge.area.marginsRemoved(
|
|
{ skip, skip, skip, skip });
|
|
_volumeToggle.icon = _volumeToggle.area.marginsRemoved(
|
|
{ skip, skip, skip, skip });
|
|
_play.icon = QRect(
|
|
rect.x() + (rect.width() - st::pipPlayIcon.width()) / 2,
|
|
rect.y() + (rect.height() - st::pipPlayIcon.height()) / 2,
|
|
st::pipPlayIcon.width(),
|
|
st::pipPlayIcon.height());
|
|
const auto volumeArea = _volumeController.area;
|
|
_volumeController.icon = (volumeArea.width() > 2 * volumeSkip
|
|
&& volumeArea.height() > 2 * volumeSkip)
|
|
? volumeArea.marginsRemoved(
|
|
{ volumeSkip, volumeSkip, volumeSkip, volumeSkip })
|
|
: QRect();
|
|
const auto playbackSkip = st::pipPlaybackSkip;
|
|
const auto playbackHeight = 2 * playbackSkip + st::pipPlaybackWide;
|
|
_playback.area = QRect(
|
|
rect.x(),
|
|
rect.y() + rect.height() - playbackHeight,
|
|
rect.width(),
|
|
playbackHeight);
|
|
_playback.icon = _playback.area.marginsRemoved(
|
|
{ playbackSkip, playbackSkip, playbackSkip, playbackSkip });
|
|
}, _panel.rp()->lifetime());
|
|
|
|
_playbackProgress->setValueChangedCallback([=](
|
|
float64 value,
|
|
float64 receivedTill) {
|
|
_panel.widget()->update(_playback.area);
|
|
});
|
|
}
|
|
|
|
void Pip::saveGeometry() {
|
|
_delegate->pipSaveGeometry(Serialize(_panel.countPosition()));
|
|
}
|
|
|
|
void Pip::updatePlayPauseResumeState(const Player::TrackState &state) {
|
|
auto showPause = Player::ShowPauseIcon(state.state);
|
|
if (showPause != _showPause) {
|
|
_showPause = showPause;
|
|
_panel.update();
|
|
}
|
|
}
|
|
|
|
void Pip::setupStreaming() {
|
|
_instance.setPriority(kPipLoaderPriority);
|
|
_instance.lockPlayer();
|
|
|
|
_instance.player().updates(
|
|
) | rpl::start_with_next_error([=](Streaming::Update &&update) {
|
|
handleStreamingUpdate(std::move(update));
|
|
}, [=](Streaming::Error &&error) {
|
|
handleStreamingError(std::move(error));
|
|
}, _instance.lifetime());
|
|
updatePlaybackState();
|
|
}
|
|
|
|
Ui::GL::ChosenRenderer Pip::chooseRenderer(
|
|
Ui::GL::Capabilities capabilities) {
|
|
const auto use = Platform::IsMac()
|
|
? true
|
|
: capabilities.transparency;
|
|
LOG(("OpenGL: %1 (PipPanel)").arg(Logs::b(use)));
|
|
if (use) {
|
|
_opengl = true;
|
|
return {
|
|
.renderer = std::make_unique<RendererGL>(this),
|
|
.backend = Ui::GL::Backend::OpenGL,
|
|
};
|
|
}
|
|
return {
|
|
.renderer = std::make_unique<RendererSW>(this),
|
|
.backend = Ui::GL::Backend::Raster,
|
|
};
|
|
}
|
|
|
|
void Pip::paint(not_null<Renderer*> renderer) const {
|
|
const auto controlsShown = _controlsShown.value(
|
|
(_over != OverState::None) ? 1. : 0.);
|
|
auto geometry = ContentGeometry{
|
|
.inner = _panel.inner(),
|
|
.attached = (_panel.useTransparency()
|
|
? _panel.attached()
|
|
: RectPart::AllSides),
|
|
.fade = controlsShown,
|
|
.outer = _panel.widget()->size(),
|
|
.rotation = _rotation,
|
|
.videoRotation = _instance.info().video.rotation,
|
|
.useTransparency = _panel.useTransparency(),
|
|
};
|
|
if (canUseVideoFrame()) {
|
|
renderer->paintTransformedVideoFrame(geometry);
|
|
_instance.markFrameShown();
|
|
} else {
|
|
const auto content = staticContent();
|
|
if (_preparedCoverState == ThumbState::Cover) {
|
|
geometry.rotation += base::take(geometry.videoRotation);
|
|
}
|
|
renderer->paintTransformedStaticContent(content, geometry);
|
|
}
|
|
if (_instance.waitingShown()) {
|
|
renderer->paintRadialLoading(countRadialRect(), controlsShown);
|
|
}
|
|
if (controlsShown > 0) {
|
|
paintButtons(renderer, controlsShown);
|
|
paintPlayback(renderer, controlsShown);
|
|
paintVolumeController(renderer, controlsShown);
|
|
}
|
|
}
|
|
|
|
void Pip::paintButtons(not_null<Renderer*> renderer, float64 shown) const {
|
|
const auto outer = _panel.widget()->width();
|
|
const auto drawOne = [&](
|
|
const Button &button,
|
|
const style::icon &icon,
|
|
const style::icon &iconOver) {
|
|
renderer->paintButton(
|
|
button,
|
|
outer,
|
|
shown,
|
|
activeValue(button),
|
|
icon,
|
|
iconOver);
|
|
};
|
|
|
|
renderer->paintButtonsStart();
|
|
drawOne(
|
|
_play,
|
|
_showPause ? st::pipPauseIcon : st::pipPlayIcon,
|
|
_showPause ? st::pipPauseIconOver : st::pipPlayIconOver);
|
|
drawOne(_close, st::pipCloseIcon, st::pipCloseIconOver);
|
|
drawOne(_enlarge, st::pipEnlargeIcon, st::pipEnlargeIconOver);
|
|
const auto volume = Core::App().settings().videoVolume();
|
|
if (volume <= 0.) {
|
|
drawOne(
|
|
_volumeToggle,
|
|
st::pipVolumeIcon0,
|
|
st::pipVolumeIcon0Over);
|
|
} else if (volume < 1 / 2.) {
|
|
drawOne(
|
|
_volumeToggle,
|
|
st::pipVolumeIcon1,
|
|
st::pipVolumeIcon1Over);
|
|
} else {
|
|
drawOne(
|
|
_volumeToggle,
|
|
st::pipVolumeIcon2,
|
|
st::pipVolumeIcon2Over);
|
|
}
|
|
}
|
|
|
|
void Pip::paintPlayback(not_null<Renderer*> renderer, float64 shown) const {
|
|
const auto outer = QRect(
|
|
_playback.icon.x(),
|
|
_playback.icon.y() - st::pipPlaybackFont->height,
|
|
_playback.icon.width(),
|
|
st::pipPlaybackFont->height + _playback.icon.height());
|
|
renderer->paintPlayback(outer, shown);
|
|
}
|
|
|
|
void Pip::paintPlaybackContent(
|
|
QPainter &p,
|
|
QRect outer,
|
|
float64 shown) const {
|
|
p.setOpacity(shown);
|
|
paintPlaybackProgress(p, outer);
|
|
paintPlaybackTexts(p, outer);
|
|
}
|
|
|
|
void Pip::paintPlaybackProgress(QPainter &p, QRect outer) const {
|
|
const auto radius = _playback.icon.height() / 2;
|
|
const auto progress = _playbackProgress->value();
|
|
const auto active = activeValue(_playback);
|
|
const auto height = anim::interpolate(
|
|
st::pipPlaybackWidth,
|
|
_playback.icon.height(),
|
|
active);
|
|
const auto rect = QRect(
|
|
outer.x(),
|
|
(outer.y()
|
|
+ st::pipPlaybackFont->height
|
|
+ _playback.icon.height()
|
|
- height),
|
|
outer.width(),
|
|
height);
|
|
|
|
paintProgressBar(p, rect, progress, radius, active);
|
|
}
|
|
|
|
void Pip::paintProgressBar(
|
|
QPainter &p,
|
|
const QRect &rect,
|
|
float64 progress,
|
|
int radius,
|
|
float64 active) const {
|
|
const auto done = int(base::SafeRound(rect.width() * progress));
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
if (done > 0) {
|
|
p.setBrush(anim::brush(
|
|
st::mediaviewPipControlsFg,
|
|
st::mediaviewPipPlaybackActive,
|
|
active));
|
|
p.setClipRect(rect.x(), rect.y(), done, rect.height());
|
|
p.drawRoundedRect(
|
|
rect.x(),
|
|
rect.y(),
|
|
std::min(done + radius, rect.width()),
|
|
rect.height(),
|
|
radius,
|
|
radius);
|
|
}
|
|
if (done < rect.width()) {
|
|
const auto from = std::max(rect.x() + done - radius, rect.x());
|
|
p.setBrush(st::mediaviewPipPlaybackInactive);
|
|
p.setClipRect(
|
|
rect.x() + done,
|
|
rect.y(),
|
|
rect.width() - done,
|
|
rect.height());
|
|
p.drawRoundedRect(
|
|
from,
|
|
rect.y(),
|
|
rect.x() + rect.width() - from,
|
|
rect.height(),
|
|
radius,
|
|
radius);
|
|
}
|
|
p.setClipping(false);
|
|
}
|
|
|
|
void Pip::paintPlaybackTexts(QPainter &p, QRect outer) const {
|
|
const auto left = outer.x()
|
|
- _playback.icon.x()
|
|
+ _playback.area.x()
|
|
+ st::pipPlaybackTextSkip;
|
|
const auto right = outer.x()
|
|
- _playback.icon.x()
|
|
+ _playback.area.x()
|
|
+ _playback.area.width()
|
|
- st::pipPlaybackTextSkip;
|
|
const auto top = outer.y() + st::pipPlaybackFont->ascent;
|
|
|
|
p.setFont(st::pipPlaybackFont);
|
|
p.setPen(st::mediaviewPipControlsFgOver);
|
|
p.drawText(left, top, _timeAlready);
|
|
p.drawText(right - _timeLeftWidth, top, _timeLeft);
|
|
}
|
|
|
|
void Pip::paintVolumeController(
|
|
not_null<Renderer*> renderer,
|
|
float64 shown) const {
|
|
if (_volumeController.icon.isEmpty()) {
|
|
return;
|
|
}
|
|
renderer->paintVolumeController(_volumeController.icon, shown);
|
|
}
|
|
|
|
void Pip::paintVolumeControllerContent(
|
|
QPainter &p,
|
|
QRect outer,
|
|
float64 shown) const {
|
|
p.setOpacity(shown);
|
|
|
|
const auto radius = _volumeController.icon.height() / 2;
|
|
const auto volume = Core::App().settings().videoVolume();
|
|
const auto active = activeValue(_volumeController);
|
|
const auto height = anim::interpolate(
|
|
st::pipPlaybackWidth,
|
|
_volumeController.icon.height(),
|
|
active);
|
|
const auto rect = QRect(
|
|
outer.x(),
|
|
outer.y() + radius - height / 2,
|
|
outer.width(),
|
|
height);
|
|
|
|
paintProgressBar(p, rect, volume, radius, active);
|
|
}
|
|
|
|
void Pip::handleStreamingUpdate(Streaming::Update &&update) {
|
|
using namespace Streaming;
|
|
|
|
v::match(update.data, [&](Information &update) {
|
|
_panel.setAspectRatio(
|
|
FlipSizeByRotation(update.video.size, _rotation));
|
|
}, [&](const PreloadedVideo &update) {
|
|
updatePlaybackState();
|
|
}, [&](const UpdateVideo &update) {
|
|
_panel.update();
|
|
Core::App().updateNonIdle();
|
|
updatePlaybackState();
|
|
}, [&](const PreloadedAudio &update) {
|
|
updatePlaybackState();
|
|
}, [&](const UpdateAudio &update) {
|
|
updatePlaybackState();
|
|
}, [&](WaitingForData) {
|
|
}, [&](MutedByOther) {
|
|
}, [&](Finished) {
|
|
updatePlaybackState();
|
|
});
|
|
}
|
|
|
|
void Pip::updatePlaybackState() {
|
|
const auto state = _instance.player().prepareLegacyState();
|
|
updatePlayPauseResumeState(state);
|
|
if (state.position == kTimeUnknown
|
|
|| state.length == kTimeUnknown
|
|
|| _pausedBySeek) {
|
|
return;
|
|
}
|
|
_playbackProgress->updateState(state);
|
|
updatePowerSaveBlocker(state);
|
|
|
|
qint64 position = 0;
|
|
if (Player::IsStoppedAtEnd(state.state)) {
|
|
position = state.length;
|
|
} else if (!Player::IsStoppedOrStopping(state.state)) {
|
|
position = state.position;
|
|
} else {
|
|
position = 0;
|
|
}
|
|
const auto playFrequency = state.frequency;
|
|
_lastDurationMs = (state.length * crl::time(1000)) / playFrequency;
|
|
|
|
if (_seekPositionMs < 0) {
|
|
updatePlaybackTexts(position, state.length, playFrequency);
|
|
}
|
|
}
|
|
|
|
void Pip::updatePowerSaveBlocker(const Player::TrackState &state) {
|
|
const auto block = _data->isVideoFile()
|
|
&& !IsPausedOrPausing(state.state)
|
|
&& !IsStoppedOrStopping(state.state);
|
|
base::UpdatePowerSaveBlocker(
|
|
_powerSaveBlocker,
|
|
block,
|
|
base::PowerSaveBlockType::PreventDisplaySleep,
|
|
[] { return u"Video playback is active"_q; },
|
|
[=] { return _panel.widget()->windowHandle(); });
|
|
}
|
|
|
|
void Pip::updatePlaybackTexts(
|
|
int64 position,
|
|
int64 length,
|
|
int64 frequency) {
|
|
const auto playAlready = position / frequency;
|
|
const auto playLeft = (length / frequency) - playAlready;
|
|
const auto already = Ui::FormatDurationText(playAlready);
|
|
const auto minus = QChar(8722);
|
|
const auto left = minus + Ui::FormatDurationText(playLeft);
|
|
if (_timeAlready == already && _timeLeft == left) {
|
|
return;
|
|
}
|
|
_timeAlready = already;
|
|
_timeLeft = left;
|
|
_timeLeftWidth = st::pipPlaybackFont->width(_timeLeft);
|
|
_panel.widget()->update(QRect(
|
|
_playback.area.x(),
|
|
_playback.icon.y() - st::pipPlaybackFont->height,
|
|
_playback.area.width(),
|
|
st::pipPlaybackFont->height));
|
|
}
|
|
|
|
void Pip::handleStreamingError(Streaming::Error &&error) {
|
|
_panel.widget()->close();
|
|
}
|
|
|
|
void Pip::playbackPauseResume() {
|
|
if (_instance.player().failed()) {
|
|
_panel.widget()->close();
|
|
} else if (_instance.player().finished()
|
|
|| !_instance.player().active()) {
|
|
_startPaused = false;
|
|
restartAtSeekPosition(0);
|
|
} else if (_instance.player().paused()) {
|
|
_instance.resume();
|
|
updatePlaybackState();
|
|
} else {
|
|
_instance.pause();
|
|
updatePlaybackState();
|
|
}
|
|
}
|
|
|
|
void Pip::restartAtSeekPosition(crl::time position) {
|
|
if (!_instance.info().video.cover.isNull()) {
|
|
_preparedCoverStorage = QImage();
|
|
_preparedCoverState = ThumbState::Empty;
|
|
_instance.saveFrameToCover();
|
|
}
|
|
|
|
auto options = Streaming::PlaybackOptions();
|
|
options.position = position;
|
|
options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo();
|
|
options.audioId = _instance.player().prepareLegacyState().id;
|
|
options.speed = _delegate->pipPlaybackSpeed();
|
|
|
|
_instance.play(options);
|
|
if (_startPaused) {
|
|
_instance.pause();
|
|
}
|
|
_pausedBySeek = false;
|
|
updatePlaybackState();
|
|
}
|
|
|
|
bool Pip::canUseVideoFrame() const {
|
|
return _instance.player().ready()
|
|
&& !_instance.info().video.cover.isNull();
|
|
}
|
|
|
|
QImage Pip::videoFrame(const FrameRequest &request) const {
|
|
Expects(canUseVideoFrame());
|
|
|
|
return _instance.frame(request);
|
|
}
|
|
|
|
Streaming::FrameWithInfo Pip::videoFrameWithInfo() const {
|
|
Expects(canUseVideoFrame());
|
|
|
|
return _instance.frameWithInfo();
|
|
}
|
|
|
|
QImage Pip::staticContent() const {
|
|
const auto &cover = _instance.info().video.cover;
|
|
const auto media = _data->activeMediaView();
|
|
const auto use = media
|
|
? media
|
|
: _data->inlineThumbnailBytes().isEmpty()
|
|
? nullptr
|
|
: _data->createMediaView();
|
|
if (use) {
|
|
use->goodThumbnailWanted();
|
|
}
|
|
const auto good = use ? use->goodThumbnail() : nullptr;
|
|
const auto thumb = use ? use->thumbnail() : nullptr;
|
|
const auto blurred = use ? use->thumbnailInline() : nullptr;
|
|
|
|
const auto state = !cover.isNull()
|
|
? ThumbState::Cover
|
|
: good
|
|
? ThumbState::Good
|
|
: thumb
|
|
? ThumbState::Thumb
|
|
: blurred
|
|
? ThumbState::Inline
|
|
: ThumbState::Empty;
|
|
if (!_preparedCoverStorage.isNull() && _preparedCoverState >= state) {
|
|
return _preparedCoverStorage;
|
|
}
|
|
_preparedCoverState = state;
|
|
if (state == ThumbState::Cover) {
|
|
_preparedCoverStorage = _instance.info().video.cover;
|
|
} else {
|
|
_preparedCoverStorage = (good
|
|
? good
|
|
: thumb
|
|
? thumb
|
|
: blurred
|
|
? blurred
|
|
: Image::BlankMedia().get())->original();
|
|
if (!good) {
|
|
_preparedCoverStorage = Images::Blur(
|
|
std::move(_preparedCoverStorage));
|
|
}
|
|
}
|
|
return _preparedCoverStorage;
|
|
}
|
|
|
|
void Pip::paintRadialLoadingContent(
|
|
QPainter &p,
|
|
const QRect &inner,
|
|
QColor fg) const {
|
|
const auto arc = inner.marginsRemoved(QMargins(
|
|
st::radialLine,
|
|
st::radialLine,
|
|
st::radialLine,
|
|
st::radialLine));
|
|
p.setOpacity(_instance.waitingOpacity());
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(st::radialBg);
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawEllipse(inner);
|
|
}
|
|
p.setOpacity(1.);
|
|
Ui::InfiniteRadialAnimation::Draw(
|
|
p,
|
|
_instance.waitingState(),
|
|
arc.topLeft(),
|
|
arc.size(),
|
|
_panel.widget()->width(),
|
|
fg,
|
|
st::radialLine);
|
|
}
|
|
|
|
QRect Pip::countRadialRect() const {
|
|
const auto outer = _panel.inner();
|
|
return {
|
|
outer.x() + (outer.width() - st::radialSize.width()) / 2,
|
|
outer.y() + (outer.height() - st::radialSize.height()) / 2,
|
|
st::radialSize.width(),
|
|
st::radialSize.height()
|
|
};
|
|
}
|
|
|
|
Pip::OverState Pip::computeState(QPoint position) const {
|
|
if (!_panel.inner().contains(position)) {
|
|
return OverState::None;
|
|
} else if (_close.area.contains(position)) {
|
|
return OverState::Close;
|
|
} else if (_enlarge.area.contains(position)) {
|
|
return OverState::Enlarge;
|
|
} else if (_playback.area.contains(position)) {
|
|
return OverState::Playback;
|
|
} else if (_volumeToggle.area.contains(position)) {
|
|
return OverState::VolumeToggle;
|
|
} else if (_volumeController.area.contains(position)) {
|
|
return OverState::VolumeController;
|
|
} else {
|
|
return OverState::Other;
|
|
}
|
|
}
|
|
|
|
void Pip::waitingAnimationCallback() {
|
|
_panel.widget()->update(countRadialRect());
|
|
}
|
|
|
|
} // namespace View
|
|
} // namespace Media
|