init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,338 @@
/*
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
*/
using "ui/basic.style";
using "ui/widgets/widgets.style";
using "overview/overview.style";
MediaPlayerButton {
playPosition: point;
playOuter: size;
pausePosition: point;
pauseOuter: size;
pauseStroke: pixels;
cancelPosition: point;
cancelOuter: size;
cancelStroke: pixels;
rippleAreaPosition: point;
rippleAreaSize: pixels;
ripple: RippleAnimation;
duration: int;
}
MediaSpeedMenu {
dropdown: DropdownMenu;
qualityMenu: Menu;
activeCheck: icon;
activeCheckSkip: pixels;
sliderStyle: TextStyle;
sliderPadding: margins;
sliderWidth: pixels;
slider: MediaSlider;
slow: icon;
slowActive: icon;
normal: icon;
normalActive: icon;
medium: icon;
mediumActive: icon;
fast: icon;
fastActive: icon;
veryFast: icon;
veryFastActive: icon;
superFast: icon;
superFastActive: icon;
}
MediaSpeedButton {
size: size;
padding: margins;
font: font;
fg: color;
overFg: color;
activeFg: color;
icon: icon;
ripple: RippleAnimation;
rippleActiveColor: color;
rippleRadius: pixels;
menu: MediaSpeedMenu;
menuAlign: align;
}
mediaPlayerButton: MediaPlayerButton {
playPosition: point(2px, 0px);
playOuter: size(17px, 15px);
pausePosition: point(1px, 1px);
pauseOuter: size(15px, 15px);
pauseStroke: 5px;
cancelPosition: point(1px, 1px);
cancelOuter: size(15px, 15px);
cancelStroke: 3px;
rippleAreaPosition: point(0px, 5px);
rippleAreaSize: 25px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgOver;
}
duration: 200;
}
mediaPlayerWideWidth: 460px;
mediaPlayerHeight: 35px;
mediaPlayerPadding: 8px;
mediaPlayerNameTop: 22px;
mediaPlayerPlayLeft: 9px;
mediaPlayerPlaySkip: 1px;
mediaPlayerPlayTop: 0px;
mediaPlayerCloseRight: 0px;
mediaPlayerName: FlatLabel(defaultFlatLabel) {
maxHeight: 20px;
}
mediaPlayerTime: LabelSimple(defaultLabelSimple) {
textFg: windowSubTextFg;
}
mediaPlayerRepeatButton: IconButton {
width: 30px;
height: 30px;
icon: icon {
{ "player/player_repeat", mediaPlayerActiveFg }
};
iconPosition: point(2px, 5px);
rippleAreaPosition: point(2px, 6px);
rippleAreaSize: 24px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgOver;
}
}
mediaPlayerRepeatDisabledIcon: icon {
{ "player/player_repeat", menuIconFg }
};
mediaPlayerRepeatDisabledIconOver: icon {
{ "player/player_repeat", menuIconFgOver }
};
mediaPlayerRepeatOneIcon: icon {
{ "player/player_repeat_single", mediaPlayerActiveFg }
};
mediaPlayerReverseIcon: icon {
{ "player/player_order", mediaPlayerActiveFg }
};
mediaPlayerReverseDisabledIcon: icon {
{ "player/player_order", menuIconFg }
};
mediaPlayerReverseDisabledIconOver: icon {
{ "player/player_order", menuIconFgOver }
};
mediaPlayerShuffleIcon: icon {
{ "player/player_shuffle", mediaPlayerActiveFg }
};
mediaPlayerOrderButton: IconButton(mediaPlayerRepeatButton) {
iconPosition: point(2px, 6px);
rippleAreaPosition: point(2px, 6px);
}
mediaPlayerRepeatDisabledRippleBg: windowBgOver;
mediaPlayerPlayButton: IconButton(mediaPlayerRepeatButton) {
width: 24px;
icon: icon{
{ "player/player_play", mediaPlayerActiveFg }
};
iconPosition: point(0px, 5px);
rippleAreaPosition: point(0px, 5px);
rippleAreaSize: 24px;
}
mediaPlayerPauseIcon: icon{
{ "player/player_pause", mediaPlayerActiveFg }
};
mediaPlayerCancelIcon: icon{
{ "player/panel_close", mediaPlayerActiveFg }
};
mediaPlayerMenu: DropdownMenu(defaultDropdownMenu) {
wrap: InnerDropdown(defaultInnerDropdown) {
scrollPadding: margins(0px, 4px, 0px, 4px);
padding: margins(10px, 2px, 10px, 10px);
}
}
mediaPlayerMenuCheck: icon {{ "player/player_check", mediaPlayerActiveFg }};
mediaPlayerSpeedMenuInner: Menu(menuWithIcons) {
separator: MenuSeparator(defaultMenuSeparator) {
padding: margins(0px, 4px, 0px, 4px);
width: 6px;
}
itemPadding: margins(54px, 7px, 54px, 9px);
itemFgDisabled: mediaPlayerActiveFg;
}
mediaPlayerSpeedMenu: MediaSpeedMenu {
dropdown: DropdownMenu(mediaPlayerMenu) {
menu: mediaPlayerSpeedMenuInner;
}
qualityMenu: Menu(mediaPlayerSpeedMenuInner) {
itemPadding: margins(17px, 7px, 54px, 9px);
}
activeCheck: mediaPlayerMenuCheck;
activeCheckSkip: 8px;
sliderStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
sliderPadding: margins(50px, 8px, 12px, 8px);
sliderWidth: 122px;
slider: MediaSlider(defaultContinuousSlider) {
activeFg: mediaPlayerActiveFg;
inactiveFg: windowBgOver;
activeFgOver: mediaPlayerActiveFg;
inactiveFgOver: windowBgOver;
activeFgDisabled: windowBgOver;
receivedTillFg: windowBgOver;
width: 6px;
seekSize: size(6px, 6px);
}
slow: playerSpeedSlow;
slowActive: playerSpeedSlowActive;
normal: playerSpeedNormal;
normalActive: playerSpeedNormalActive;
medium: playerSpeedMedium;
mediumActive: playerSpeedMediumActive;
fast: playerSpeedFast;
fastActive: playerSpeedFastActive;
veryFast: playerSpeedVeryFast;
veryFastActive: playerSpeedVeryFastActive;
superFast: playerSpeedSuperFast;
superFastActive: playerSpeedSuperFastActive;
}
mediaPlayerSpeedButton: MediaSpeedButton {
size: size(30px, 30px);
padding: margins(0px, 6px, 0px, 0px);
font: font(11px bold);
fg: menuIconFg;
overFg: menuIconFgOver;
activeFg: mediaPlayerActiveFg;
icon: icon{{ "player/player_speed", menuIconFg }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
rippleActiveColor: lightButtonBgOver;
rippleRadius: 4px;
menu: mediaPlayerSpeedMenu;
menuAlign: align(topright);
}
mediaPlayerVolumeIcon0: icon {
{ "player/player_mini_off", mediaPlayerActiveFg },
};
mediaPlayerVolumeIcon1: icon {
{ "player/player_mini_half", mediaPlayerActiveFg },
};
mediaPlayerVolumeToggle: IconButton(mediaPlayerRepeatButton) {
width: 34px;
icon: icon {
{ "player/player_mini_full", mediaPlayerActiveFg },
};
iconPosition: point(5px, 6px);
rippleAreaPosition: point(5px, 6px);
}
mediaPlayerVolumeMargin: 10px;
mediaPlayerVolumeSize: size(27px, 100px);
mediaPlayerControlsFade: icon {{ "fade_horizontal", mediaPlayerBg }};
mediaPlayerNextButton: IconButton(mediaPlayerPlayButton) {
icon: icon {
{ "player/player_forward", mediaPlayerActiveFg },
};
}
mediaPlayerNextDisabledIcon: icon {
{ "player/player_forward", mediaPlayerInactiveFg },
};
mediaPlayerPreviousButton: IconButton(mediaPlayerNextButton) {
icon: icon {
{ "player/player_backward", mediaPlayerActiveFg },
};
}
mediaPlayerPreviousDisabledIcon: icon {
{ "player/player_backward", mediaPlayerInactiveFg },
};
touchBarIconPlayerClose: icon {{ "player/panel_close", windowFg }};
touchBarIconPlayerPlay: icon {{ "media_play", windowFg }};
touchBarIconPlayerPause: icon {{ "media_pause", windowFg }};
touchBarIconPlayerNext: icon {{ "player/player_forward", windowFg }};
touchBarIconPlayerPrevious: icon {{ "player/player_backward", windowFg }};
mediaPlayerClose: IconButton(mediaPlayerRepeatButton) {
width: 39px;
icon: icon {{ "player/panel_close", menuIconFg }};
iconOver: icon {{ "player/panel_close", menuIconFgOver }};
iconPosition: point(4px, 6px);
rippleAreaPosition: point(4px, 6px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
}
mediaPlayerPlayback: FilledSlider {
fullWidth: 6px;
lineWidth: 2px;
activeFg: mediaPlayerActiveFg;
inactiveFg: mediaPlayerInactiveFg;
disabledFg: mediaPlayerDisabledFg;
duration: 150;
}
mediaPlayerPanelMarginLeft: 10px;
mediaPlayerPanelMarginBottom: 10px;
mediaPlayerPanelWidth: 344px;
mediaPlayerPanelNextButton: IconButton(mediaPlayerRepeatButton) {
width: 37px;
icon: icon {{ "player/player_forward", mediaPlayerActiveFg, point(6px, 4px) }};
}
mediaPlayerPanelPlaybackPadding: 8px;
mediaPlayerPanelPlayback: defaultContinuousSlider;
mediaPlayerPanelVolumeWidth: 64px;
mediaPlayerScroll: ScrollArea(defaultSolidScroll) {
deltat: 10px;
deltab: 10px;
}
mediaPlayerListHeightMax: 280px;
mediaPlayerListMarginBottom: 10px;
mediaPlayerFileLayout: OverviewFileLayout(overviewFileLayout) {
maxWidth: 344px;
songIconBg: mediaPlayerActiveFg;
songOverBg: mediaPlayerActiveFg;
}
mediaPlayerFloatSize: 128px;
mediaPlayerFloatMargin: 12px;
mediaPlayerMenuPosition: point(-2px, -1px);
mediaPlayerOrderMenu: Menu(defaultMenu) {
itemIconPosition: point(13px, 8px);
itemPadding: margins(49px, 9px, 17px, 11px);
itemStyle: boxTextStyle;
}
mediaPlayerOrderMenuActive: Menu(mediaPlayerOrderMenu) {
itemFg: windowActiveTextFg;
itemFgOver: windowActiveTextFg;
}
mediaPlayerOrderIconReverse: icon{{ "player/player_order", windowFg }};
mediaPlayerOrderIconReverseActive: icon{{ "player/player_order", windowActiveTextFg }};
mediaPlayerOrderIconShuffle: icon{{ "player/player_shuffle", windowFg }};
mediaPlayerOrderIconShuffleActive: icon{{ "player/player_shuffle", windowActiveTextFg }};

View File

@@ -0,0 +1,505 @@
/*
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/player/media_player_button.h"
#include "media/media_common.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "styles/style_media_player.h"
#include "styles/style_media_view.h"
#include <QtCore/QtMath>
namespace Media::Player {
namespace {
[[nodiscard]] QString SpeedText(float64 speed) {
return QString::number(base::SafeRound(speed * 10) / 10.) + 'X';
}
} // namespace
PlayButtonLayout::PlayButtonLayout(
const style::MediaPlayerButton &st,
Fn<void()> callback)
: _st(st)
, _callback(std::move(callback)) {
}
void PlayButtonLayout::setState(State state) {
if (_nextState == state) {
return;
}
_nextState = state;
if (!_transformProgress.animating()) {
_oldState = _state;
_state = _nextState;
_transformBackward = false;
if (_state != _oldState) {
startTransform(0., 1.);
if (_callback) _callback();
}
} else if (_oldState == _nextState) {
qSwap(_oldState, _state);
startTransform(_transformBackward ? 0. : 1., _transformBackward ? 1. : 0.);
_transformBackward = !_transformBackward;
}
}
void PlayButtonLayout::finishTransform() {
_transformProgress.stop();
_transformBackward = false;
if (_callback) _callback();
}
void PlayButtonLayout::paint(QPainter &p, const QBrush &brush) {
if (_transformProgress.animating()) {
auto from = _oldState, to = _state;
auto backward = _transformBackward;
auto progress = _transformProgress.value(1.);
if (from == State::Cancel || (from == State::Pause && to == State::Play)) {
qSwap(from, to);
backward = !backward;
}
if (backward) progress = 1. - progress;
Assert(from != to);
if (from == State::Play) {
if (to == State::Pause) {
paintPlayToPause(p, brush, progress);
} else {
Assert(to == State::Cancel);
paintPlayToCancel(p, brush, progress);
}
} else {
Assert(from == State::Pause && to == State::Cancel);
paintPauseToCancel(p, brush, progress);
}
} else {
switch (_state) {
case State::Play: paintPlay(p, brush); break;
case State::Pause: paintPlayToPause(p, brush, 1.); break;
case State::Cancel: paintPlayToCancel(p, brush, 1.); break;
}
}
}
void PlayButtonLayout::paintPlay(QPainter &p, const QBrush &brush) {
auto playLeft = 0. + _st.playPosition.x();
auto playTop = 0. + _st.playPosition.y();
auto playWidth = _st.playOuter.width() - 2 * playLeft;
auto playHeight = _st.playOuter.height() - 2 * playTop;
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
QPainterPath pathPlay;
pathPlay.moveTo(playLeft, playTop);
pathPlay.lineTo(playLeft + playWidth, playTop + (playHeight / 2.));
pathPlay.lineTo(playLeft, playTop + playHeight);
pathPlay.lineTo(playLeft, playTop);
p.fillPath(pathPlay, brush);
}
void PlayButtonLayout::paintPlayToPause(QPainter &p, const QBrush &brush, float64 progress) {
auto playLeft = 0. + _st.playPosition.x();
auto playTop = 0. + _st.playPosition.y();
auto playWidth = _st.playOuter.width() - 2 * playLeft;
auto playHeight = _st.playOuter.height() - 2 * playTop;
auto pauseLeft = 0. + _st.pausePosition.x();
auto pauseTop = 0. + _st.pausePosition.y();
auto pauseWidth = _st.pauseOuter.width() - 2 * pauseLeft;
auto pauseHeight = _st.pauseOuter.height() - 2 * pauseTop;
auto pauseStroke = 0. + _st.pauseStroke;
p.setPen(Qt::NoPen);
PainterHighQualityEnabler hq(p);
QPointF pathLeftPause[] = {
{ pauseLeft, pauseTop },
{ pauseLeft + pauseStroke, pauseTop },
{ pauseLeft + pauseStroke, pauseTop + pauseHeight },
{ pauseLeft, pauseTop + pauseHeight },
};
QPointF pathLeftPlay[] = {
{ playLeft, playTop },
{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },
{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },
{ playLeft, playTop + playHeight },
};
p.fillPath(anim::interpolate(pathLeftPlay, pathLeftPause, progress), brush);
QPointF pathRightPause[] = {
{ pauseLeft + pauseWidth - pauseStroke, pauseTop },
{ pauseLeft + pauseWidth, pauseTop },
{ pauseLeft + pauseWidth, pauseTop + pauseHeight },
{ pauseLeft + pauseWidth - pauseStroke, pauseTop + pauseHeight },
};
QPointF pathRightPlay[] = {
{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },
};
p.fillPath(anim::interpolate(pathRightPlay, pathRightPause, progress), brush);
}
void PlayButtonLayout::paintPlayToCancel(QPainter &p, const QBrush &brush, float64 progress) {
auto playLeft = 0. + _st.playPosition.x();
auto playTop = 0. + _st.playPosition.y();
auto playWidth = _st.playOuter.width() - 2 * playLeft;
auto playHeight = _st.playOuter.height() - 2 * playTop;
auto cancelLeft = 0. + _st.cancelPosition.x();
auto cancelTop = 0. + _st.cancelPosition.y();
auto cancelWidth = _st.cancelOuter.width() - 2 * cancelLeft;
auto cancelHeight = _st.cancelOuter.height() - 2 * cancelTop;
auto cancelStroke = (0. + _st.cancelStroke) / M_SQRT2;
p.setPen(Qt::NoPen);
PainterHighQualityEnabler hq(p);
QPointF pathPlay[] = {
{ playLeft, playTop },
{ playLeft, playTop },
{ playLeft + (playWidth / 2.), playTop + (playHeight / 4.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + playWidth, playTop + (playHeight / 2.) },
{ playLeft + (playWidth / 2.), playTop + (3 * playHeight / 4.) },
{ playLeft, playTop + playHeight },
{ playLeft, playTop + playHeight },
{ playLeft, playTop + (playHeight / 2.) },
};
QPointF pathCancel[] = {
{ cancelLeft, cancelTop + cancelStroke },
{ cancelLeft + cancelStroke, cancelTop },
{ cancelLeft + (cancelWidth / 2.), cancelTop + (cancelHeight / 2.) - cancelStroke },
{ cancelLeft + cancelWidth - cancelStroke, cancelTop },
{ cancelLeft + cancelWidth, cancelTop + cancelStroke },
{ cancelLeft + (cancelWidth / 2.) + cancelStroke, cancelTop + (cancelHeight / 2.) },
{ cancelLeft + cancelWidth, cancelTop + cancelHeight - cancelStroke },
{ cancelLeft + cancelWidth - cancelStroke, cancelTop + cancelHeight },
{ cancelLeft + (cancelWidth / 2.), cancelTop + (cancelHeight / 2.) + cancelStroke },
{ cancelLeft + cancelStroke, cancelTop + cancelHeight },
{ cancelLeft, cancelTop + cancelHeight - cancelStroke },
{ cancelLeft + (cancelWidth / 2.) - cancelStroke, cancelTop + (cancelHeight / 2.) },
};
p.fillPath(anim::interpolate(pathPlay, pathCancel, progress), brush);
}
void PlayButtonLayout::paintPauseToCancel(QPainter &p, const QBrush &brush, float64 progress) {
auto pauseLeft = 0. + _st.pausePosition.x();
auto pauseTop = 0. + _st.pausePosition.y();
auto pauseWidth = _st.pauseOuter.width() - 2 * pauseLeft;
auto pauseHeight = _st.pauseOuter.height() - 2 * pauseTop;
auto pauseStroke = 0. + _st.pauseStroke;
auto cancelLeft = 0. + _st.cancelPosition.x();
auto cancelTop = 0. + _st.cancelPosition.y();
auto cancelWidth = _st.cancelOuter.width() - 2 * cancelLeft;
auto cancelHeight = _st.cancelOuter.height() - 2 * cancelTop;
auto cancelStroke = (0. + _st.cancelStroke) / M_SQRT2;
p.setPen(Qt::NoPen);
PainterHighQualityEnabler hq(p);
QPointF pathLeftPause[] = {
{ pauseLeft, pauseTop },
{ pauseLeft + pauseStroke, pauseTop },
{ pauseLeft + pauseStroke, pauseTop + pauseHeight },
{ pauseLeft, pauseTop + pauseHeight },
};
QPointF pathLeftCancel[] = {
{ cancelLeft, cancelTop + cancelStroke },
{ cancelLeft + cancelStroke, cancelTop },
{ cancelLeft + cancelWidth, cancelTop + cancelHeight - cancelStroke },
{ cancelLeft + cancelWidth - cancelStroke, cancelTop + cancelHeight },
};
p.fillPath(anim::interpolate(pathLeftPause, pathLeftCancel, progress), brush);
QPointF pathRightPause[] = {
{ pauseLeft + pauseWidth - pauseStroke, pauseTop },
{ pauseLeft + pauseWidth, pauseTop },
{ pauseLeft + pauseWidth, pauseTop + pauseHeight },
{ pauseLeft + pauseWidth - pauseStroke, pauseTop + pauseHeight },
};
QPointF pathRightCancel[] = {
{ cancelLeft + cancelWidth - cancelStroke, cancelTop },
{ cancelLeft + cancelWidth, cancelTop + cancelStroke },
{ cancelLeft + cancelStroke, cancelTop + cancelHeight },
{ cancelLeft, cancelTop + cancelHeight - cancelStroke },
};
p.fillPath(anim::interpolate(pathRightPause, pathRightCancel, progress), brush);
}
void PlayButtonLayout::animationCallback() {
if (!_transformProgress.animating()) {
auto finalState = _nextState;
_nextState = _state;
setState(finalState);
}
_callback();
}
void PlayButtonLayout::startTransform(float64 from, float64 to) {
_transformProgress.start(
[=] { animationCallback(); },
from,
to,
_st.duration);
}
SpeedButtonLayout::SpeedButtonLayout(
const style::MediaSpeedButton &st,
Fn<void()> callback,
float64 speed)
: _st(st)
, _speed(speed)
, _metrics(_st.font->f)
, _text(SpeedText(speed))
, _textWidth(_metrics.horizontalAdvance(_text))
, _callback(std::move(callback)) {
const auto result = style::FindAdjustResult(_st.font->f);
_adjustedAscent = result ? result->ascent : _metrics.ascent();
_adjustedHeight = result ? result->height : _metrics.height();
}
void SpeedButtonLayout::setSpeed(float64 speed) {
speed = base::SafeRound(speed * 10.) / 10.;
if (!EqualSpeeds(_speed, speed)) {
_speed = speed;
_text = SpeedText(_speed);
_textWidth = _metrics.horizontalAdvance(_text);
if (_callback) _callback();
}
}
void SpeedButtonLayout::paint(QPainter &p, bool over, bool active) {
const auto &color = active ? _st.activeFg : over ? _st.overFg : _st.fg;
const auto inner = QRect(QPoint(), _st.size).marginsRemoved(_st.padding);
_st.icon.paintInCenter(p, inner, color->c);
p.setPen(color);
p.setFont(_st.font);
p.drawText(
QPointF(inner.topLeft()) + QPointF(
(inner.width() - _textWidth) / 2.,
(inner.height() - _adjustedHeight) / 2. + _adjustedAscent),
_text);
}
SpeedButton::SpeedButton(QWidget *parent, const style::MediaSpeedButton &st)
: RippleButton(parent, st.ripple)
, _st(st)
, _layout(st, [=] { update(); }, 2.)
, _isDefault(true) {
resize(_st.size);
}
void SpeedButton::setSpeed(float64 speed) {
_isDefault = EqualSpeeds(speed, 1.);
_layout.setSpeed(speed);
update();
}
void SpeedButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
paintRipple(
p,
QPoint(_st.padding.left(), _st.padding.top()),
_isDefault ? nullptr : &_st.rippleActiveColor->c);
_layout.paint(p, isOver(), !_isDefault);
}
QPoint SpeedButton::prepareRippleStartPosition() const {
const auto inner = rect().marginsRemoved(_st.padding);
const auto result = mapFromGlobal(QCursor::pos()) - inner.topLeft();
return inner.contains(result)
? result
: DisabledRippleStartPosition();
}
QImage SpeedButton::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(
rect().marginsRemoved(_st.padding).size(),
_st.rippleRadius);
}
SettingsButton::SettingsButton(
QWidget *parent,
const style::MediaSpeedButton &st)
: RippleButton(parent, st.ripple)
, _st(st)
, _isDefaultSpeed(true) {
resize(_st.size);
}
void SettingsButton::setSpeed(float64 speed) {
if (_speed != speed) {
_speed = speed;
_isDefaultSpeed = EqualSpeeds(speed, 1.);
update();
}
}
void SettingsButton::setQuality(int quality) {
if (_quality != quality) {
_quality = quality;
update();
}
}
void SettingsButton::setActive(bool active) {
if (_active == active) {
return;
}
_active = active;
_activeAnimation.start([=] {
update();
}, active ? 0. : 1., active ? 1. : 0., st::mediaviewOverDuration);
}
void SettingsButton::onStateChanged(State was, StateChangeSource source) {
RippleButton::onStateChanged(was, source);
const auto nowOver = isOver();
const auto wasOver = static_cast<bool>(was & StateFlag::Over);
if (nowOver != wasOver) {
_overAnimation.start([=] {
update();
}, nowOver ? 0. : 1., nowOver ? 1. : 0., st::mediaviewOverDuration);
}
}
void SettingsButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
paintRipple(
p,
QPoint(_st.padding.left(), _st.padding.top()),
_isDefaultSpeed ? nullptr : &_st.rippleActiveColor->c);
prepareFrame();
p.drawImage(0, 0, _frameCache);
}
void SettingsButton::prepareFrame() {
const auto ratio = style::DevicePixelRatio();
if (_frameCache.size() != _st.size * ratio) {
_frameCache = QImage(
_st.size * ratio,
QImage::Format_ARGB32_Premultiplied);
_frameCache.setDevicePixelRatio(ratio);
}
_frameCache.fill(Qt::transparent);
auto p = QPainter(&_frameCache);
const auto inner = QRect(
QPoint(),
_st.size
).marginsRemoved(_st.padding);
auto hq = std::optional<PainterHighQualityEnabler>();
const auto over = _overAnimation.value(isOver() ? 1. : 0.);
const auto color = anim::color(_st.fg, _st.overFg, over);
const auto active = _activeAnimation.value(_active ? 1. : 0.);
if (active > 0.) {
const auto shift = QRectF(inner).center();
p.save();
p.translate(shift);
p.rotate(active * 60.);
p.translate(-shift);
hq.emplace(p);
}
_st.icon.paintInCenter(p, inner, color);
if (active > 0.) {
p.restore();
hq.reset();
}
const auto rounded = int(base::SafeRound(_speed * 10));
if (rounded != 10) {
const auto text = (rounded % 10)
? QString::number(rounded / 10.)
: u"%1X"_q.arg(rounded / 10);
paintBadge(p, text, RectPart::TopLeft, color);
}
const auto text = (!_quality)
? QString()
: (_quality > 2000)
? u"4K"_q
: (_quality > 1000)
? u"FHD"_q
: (_quality > 700)
? u"HD"_q
: u"SD"_q;
if (!text.isEmpty()) {
paintBadge(p, text, RectPart::BottomRight, color);
}
}
void SettingsButton::paintBadge(
QPainter &p,
const QString &text,
RectPart origin,
QColor color) {
auto hq = PainterHighQualityEnabler(p);
const auto xpadding = style::ConvertScale(2.);
const auto ypadding = 0;
const auto skip = style::ConvertScale(2.);
const auto width = _st.font->width(text);
const auto height = _st.font->height;
const auto radius = height / 3.;
const auto left = (origin == RectPart::TopLeft)
|| (origin == RectPart::BottomLeft);
const auto top = (origin == RectPart::TopLeft)
|| (origin == RectPart::TopRight);
const auto x = left ? 0 : (_st.size.width() - width - 2 * xpadding);
const auto y = top
? skip
: (_st.size.height() - height - 2 * ypadding - skip);
p.setCompositionMode(QPainter::CompositionMode_Source);
const auto stroke = style::ConvertScaleExact(1.);
p.setPen(QPen(Qt::transparent, stroke));
p.setFont(_st.font);
p.setBrush(color);
p.drawRoundedRect(
QRectF(
x - stroke / 2.,
y - stroke / 2.,
width + 2 * xpadding + stroke,
height + 2 * ypadding + stroke),
radius,
radius);
p.setPen(Qt::transparent);
p.drawText(x + xpadding, y + ypadding + _st.font->ascent, text);
}
QPoint SettingsButton::prepareRippleStartPosition() const {
const auto inner = rect().marginsRemoved(_st.padding);
const auto result = mapFromGlobal(QCursor::pos()) - inner.topLeft();
return inner.contains(result)
? result
: DisabledRippleStartPosition();
}
QImage SettingsButton::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(
rect().marginsRemoved(_st.padding).size(),
_st.rippleRadius);
}
} // namespace Media::Player

View File

@@ -0,0 +1,143 @@
/*
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
#include "ui/effects/animations.h"
#include "ui/widgets/buttons.h"
#include "ui/rect_part.h"
#include <QtGui/QFontMetrics>
namespace style {
struct MediaPlayerButton;
struct MediaSpeedButton;
} // namespace style
namespace Media::Player {
class PlayButtonLayout {
public:
enum class State {
Play,
Pause,
Cancel,
};
PlayButtonLayout(const style::MediaPlayerButton &st, Fn<void()> callback);
void setState(State state);
void finishTransform();
void paint(QPainter &p, const QBrush &brush);
private:
void animationCallback();
void startTransform(float64 from, float64 to);
void paintPlay(QPainter &p, const QBrush &brush);
void paintPlayToPause(QPainter &p, const QBrush &brush, float64 progress);
void paintPlayToCancel(QPainter &p, const QBrush &brush, float64 progress);
void paintPauseToCancel(QPainter &p, const QBrush &brush, float64 progress);
const style::MediaPlayerButton &_st;
State _state = State::Play;
State _oldState = State::Play;
State _nextState = State::Play;
Ui::Animations::Simple _transformProgress;
bool _transformBackward = false;
Fn<void()> _callback;
};
class SpeedButtonLayout {
public:
SpeedButtonLayout(
const style::MediaSpeedButton &st,
Fn<void()> callback,
float64 speed);
void setSpeed(float64 speed);
void paint(QPainter &p, bool over, bool active);
private:
const style::MediaSpeedButton &_st;
float64 _speed = 1.;
QFontMetricsF _metrics;
float64 _adjustedAscent = 0.;
float64 _adjustedHeight = 0.;
QString _text;
float64 _textWidth = 0;
Fn<void()> _callback;
};
class SpeedButton final : public Ui::RippleButton {
public:
SpeedButton(QWidget *parent, const style::MediaSpeedButton &st);
[[nodiscard]] const style::MediaSpeedButton &st() const {
return _st;
}
void setSpeed(float64 speed);
private:
void paintEvent(QPaintEvent *e) override;
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
const style::MediaSpeedButton &_st;
SpeedButtonLayout _layout;
bool _isDefault = false;
};
class SettingsButton final : public Ui::RippleButton {
public:
SettingsButton(QWidget *parent, const style::MediaSpeedButton &st);
[[nodiscard]] const style::MediaSpeedButton &st() const {
return _st;
}
void setSpeed(float64 speed);
void setQuality(int quality);
void setActive(bool active);
private:
void paintEvent(QPaintEvent *e) override;
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
void onStateChanged(State was, StateChangeSource source) override;
void paintBadge(
QPainter &p,
const QString &text,
RectPart origin,
QColor color);
void prepareFrame();
const style::MediaSpeedButton &_st;
Ui::Animations::Simple _activeAnimation;
Ui::Animations::Simple _overAnimation;
QImage _frameCache;
float _speed = 1.;
int _quality = 0;
bool _isDefaultSpeed = false;
bool _active = false;
};
} // namespace Media::Player

View File

@@ -0,0 +1,865 @@
/*
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/player/media_player_dropdown.h"
#include "base/invoke_queued.h"
#include "base/timer.h"
#include "lang/lang_keys.h"
#include "media/player/media_player_button.h"
#include "ui/cached_round_corners.h"
#include "ui/widgets/menu/menu.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_media_player.h"
#include "styles/style_widgets.h"
namespace Media::Player {
namespace {
constexpr auto kSpeedDebounceTimeout = crl::time(1000);
[[nodiscard]] float64 SpeedToSliderValue(float64 speed) {
return (speed - kSpeedMin) / (kSpeedMax - kSpeedMin);
}
[[nodiscard]] float64 SliderValueToSpeed(float64 value) {
const auto speed = value * (kSpeedMax - kSpeedMin) + kSpeedMin;
return base::SafeRound(speed * 10) / 10.;
}
constexpr auto kSpeedStickedValues
= std::array<std::pair<float64, float64>, 7>{{
{ 0.8, 0.05 },
{ 1.0, 0.05 },
{ 1.2, 0.05 },
{ 1.5, 0.05 },
{ 1.7, 0.05 },
{ 2.0, 0.05 },
{ 2.2, 0.05 },
}};
class SpeedSliderItem final : public Ui::Menu::ItemBase {
public:
SpeedSliderItem(
not_null<RpWidget*> parent,
const style::MediaSpeedMenu &st,
rpl::producer<float64> value);
not_null<QAction*> action() const override;
bool isEnabled() const override;
[[nodiscard]] float64 current() const;
[[nodiscard]] rpl::producer<float64> changing() const;
[[nodiscard]] rpl::producer<float64> changed() const;
[[nodiscard]] rpl::producer<float64> debouncedChanges() const;
protected:
int contentHeight() const override;
private:
void setExternalValue(float64 speed);
void setSliderValue(float64 speed);
const base::unique_qptr<Ui::MediaSlider> _slider;
const not_null<QAction*> _dummyAction;
const style::MediaSpeedMenu &_st;
Ui::Text::String _text;
int _height = 0;
rpl::event_stream<float64> _changing;
rpl::event_stream<float64> _changed;
rpl::event_stream<float64> _debounced;
base::Timer _debounceTimer;
rpl::variable<float64> _last = 0.;
};
SpeedSliderItem::SpeedSliderItem(
not_null<RpWidget*> parent,
const style::MediaSpeedMenu &st,
rpl::producer<float64> value)
: Ui::Menu::ItemBase(parent, st.dropdown.menu)
, _slider(base::make_unique_q<Ui::MediaSlider>(this, st.slider))
, _dummyAction(new QAction(parent))
, _st(st)
, _height(st.sliderPadding.top()
+ st.dropdown.menu.itemStyle.font->height
+ st.sliderPadding.bottom())
, _debounceTimer([=] { _debounced.fire(current()); }) {
initResizeHook(parent->sizeValue());
enableMouseSelecting();
enableMouseSelecting(_slider.get());
setPointerCursor(false);
setMinWidth(st.sliderPadding.left()
+ st.sliderWidth
+ st.sliderPadding.right());
_slider->setAlwaysDisplayMarker(true);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
const auto geometry = QRect(QPoint(), size);
const auto padding = _st.sliderPadding;
const auto inner = geometry - padding;
_slider->setGeometry(
padding.left(),
inner.y(),
(geometry.width() - padding.left() - padding.right()),
inner.height());
}, lifetime());
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = Painter(this);
p.fillRect(clip, _st.dropdown.menu.itemBg);
const auto left = (_st.sliderPadding.left() - _text.maxWidth()) / 2;
const auto top = _st.dropdown.menu.itemPadding.top();
p.setPen(_st.dropdown.menu.itemFg);
_text.drawLeftElided(p, left, top, _text.maxWidth(), width());
}, lifetime());
_slider->setChangeProgressCallback([=](float64 value) {
const auto speed = SliderValueToSpeed(value);
if (!EqualSpeeds(current(), speed)) {
_last = speed;
_changing.fire_copy(speed);
_debounceTimer.callOnce(kSpeedDebounceTimeout);
}
});
_slider->setChangeFinishedCallback([=](float64 value) {
const auto speed = SliderValueToSpeed(value);
_last = speed;
_changed.fire_copy(speed);
_debounced.fire_copy(speed);
_debounceTimer.cancel();
});
std::move(
value
) | rpl::on_next([=](float64 external) {
setExternalValue(external);
}, lifetime());
_last.value(
) | rpl::on_next([=](float64 value) {
const auto text = QString::number(value, 'f', 1) + 'x';
if (_text.toString() != text) {
_text.setText(_st.sliderStyle, text);
update();
}
}, lifetime());
_slider->setAdjustCallback([=](float64 value) {
const auto speed = SliderValueToSpeed(value);
for (const auto &snap : kSpeedStickedValues) {
if (speed > (snap.first - snap.second)
&& speed < (snap.first + snap.second)) {
return SpeedToSliderValue(snap.first);
}
}
return value;
});
}
void FillSpeedMenu(
not_null<Ui::Menu::Menu*> menu,
const style::MediaSpeedMenu &st,
rpl::producer<float64> value,
Fn<void(float64)> callback,
bool onlySlider) {
auto slider = base::make_unique_q<SpeedSliderItem>(
menu,
st,
rpl::duplicate(value));
slider->debouncedChanges(
) | rpl::on_next(callback, slider->lifetime());
struct State {
rpl::variable<float64> realtime;
};
const auto state = slider->lifetime().make_state<State>();
state->realtime = rpl::single(
slider->current()
) | rpl::then(rpl::merge(
slider->changing(),
slider->changed()
));
menu->addAction(std::move(slider));
if (onlySlider) {
return;
}
menu->addSeparator(&st.dropdown.menu.separator);
struct SpeedPoint {
float64 speed = 0.;
tr::phrase<> text;
const style::icon &icon;
const style::icon &iconActive;
};
const auto points = std::vector<SpeedPoint>{
{
0.5,
tr::lng_voice_speed_slow,
st.slow,
st.slowActive },
{
1.0,
tr::lng_voice_speed_normal,
st.normal,
st.normalActive },
{
1.2,
tr::lng_voice_speed_medium,
st.medium,
st.mediumActive },
{
1.5,
tr::lng_voice_speed_fast,
st.fast,
st.fastActive },
{
1.7,
tr::lng_voice_speed_very_fast,
st.veryFast,
st.veryFastActive },
{
2.0,
tr::lng_voice_speed_super_fast,
st.superFast,
st.superFastActive },
};
for (const auto &point : points) {
const auto speed = point.speed;
const auto text = point.text(tr::now);
const auto icon = &point.icon;
const auto iconActive = &point.iconActive;
auto action = base::make_unique_q<Ui::Menu::Action>(
menu,
st.dropdown.menu,
Ui::Menu::CreateAction(menu, text, [=] { callback(speed); }),
&point.icon,
&point.icon);
const auto raw = action.get();
const auto check = Ui::CreateChild<Ui::RpWidget>(raw);
check->resize(st.activeCheck.size());
check->paintRequest(
) | rpl::on_next([check, icon = &st.activeCheck] {
auto p = QPainter(check);
icon->paint(p, 0, 0, check->width());
}, check->lifetime());
raw->sizeValue(
) | rpl::on_next([=, skip = st.activeCheckSkip](QSize size) {
check->moveToRight(
skip,
(size.height() - check->height()) / 2,
size.width());
}, check->lifetime());
check->setAttribute(Qt::WA_TransparentForMouseEvents);
state->realtime.value(
) | rpl::on_next([=](float64 now) {
const auto chosen = EqualSpeeds(speed, now);
const auto overriden = chosen ? iconActive : icon;
raw->setIcon(overriden, overriden);
raw->action()->setEnabled(!chosen);
check->setVisible(chosen);
}, raw->lifetime());
menu->addAction(std::move(action));
}
}
void SpeedSliderItem::setExternalValue(float64 speed) {
if (!_slider->isChanging()) {
setSliderValue(speed);
}
}
void SpeedSliderItem::setSliderValue(float64 speed) {
const auto value = SpeedToSliderValue(speed);
_slider->setValue(value);
_last = speed;
_changed.fire_copy(speed);
}
not_null<QAction*> SpeedSliderItem::action() const {
return _dummyAction;
}
bool SpeedSliderItem::isEnabled() const {
return false;
}
int SpeedSliderItem::contentHeight() const {
return _height;
}
float64 SpeedSliderItem::current() const {
return _last.current();
}
rpl::producer<float64> SpeedSliderItem::changing() const {
return _changing.events();
}
rpl::producer<float64> SpeedSliderItem::changed() const {
return _changed.events();
}
rpl::producer<float64> SpeedSliderItem::debouncedChanges() const {
return _debounced.events();
}
} // namespace
Dropdown::Dropdown(QWidget *parent)
: RpWidget(parent)
, _hideTimer([=] { startHide(); })
, _showTimer([=] { startShow(); }) {
hide();
macWindowDeactivateEvents(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::on_next([=] {
leaveEvent(nullptr);
}, lifetime());
hide();
auto margin = getMargin();
resize(margin.left() + st::mediaPlayerVolumeSize.width() + margin.right(), margin.top() + st::mediaPlayerVolumeSize.height() + margin.bottom());
}
QMargins Dropdown::getMargin() const {
const auto top1 = st::mediaPlayerHeight
+ st::lineWidth
- st::mediaPlayerPlayTop
- st::mediaPlayerVolumeToggle.height;
const auto top2 = st::mediaPlayerPlayback.fullWidth;
const auto top = std::max(top1, top2);
return QMargins(st::mediaPlayerVolumeMargin, top, st::mediaPlayerVolumeMargin, st::mediaPlayerVolumeMargin);
}
bool Dropdown::overlaps(const QRect &globalRect) {
if (isHidden() || _a_appearance.animating()) return false;
return rect().marginsRemoved(getMargin()).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
}
void Dropdown::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (!_cache.isNull()) {
bool animating = _a_appearance.animating();
if (animating) {
p.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));
} else if (_hiding || isHidden()) {
hidingFinished();
return;
}
p.drawPixmap(0, 0, _cache);
if (!animating) {
showChildren();
_cache = QPixmap();
}
return;
}
// draw shadow
auto shadowedRect = rect().marginsRemoved(getMargin());
auto shadowedSides = RectPart::Left | RectPart::Right | RectPart::Bottom;
Ui::Shadow::paint(p, shadowedRect, width(), st::defaultRoundShadow, shadowedSides);
const auto &corners = Ui::CachedCornerPixmaps(Ui::MenuCorners);
const auto fill = Ui::CornersPixmaps{
.p = { QPixmap(), QPixmap(), corners.p[2], corners.p[3] },
};
Ui::FillRoundRect(
p,
shadowedRect.x(),
0,
shadowedRect.width(),
shadowedRect.y() + shadowedRect.height(),
st::menuBg,
fill);
}
void Dropdown::enterEventHook(QEnterEvent *e) {
_hideTimer.cancel();
if (_a_appearance.animating()) {
startShow();
} else {
_showTimer.callOnce(0);
}
return RpWidget::enterEventHook(e);
}
void Dropdown::leaveEventHook(QEvent *e) {
_showTimer.cancel();
if (_a_appearance.animating()) {
startHide();
} else {
_hideTimer.callOnce(300);
}
return RpWidget::leaveEventHook(e);
}
void Dropdown::otherEnter() {
_hideTimer.cancel();
if (_a_appearance.animating()) {
startShow();
} else {
_showTimer.callOnce(0);
}
}
void Dropdown::otherLeave() {
_showTimer.cancel();
if (_a_appearance.animating()) {
startHide();
} else {
_hideTimer.callOnce(0);
}
}
void Dropdown::startShow() {
if (isHidden()) {
show();
} else if (!_hiding) {
return;
}
_hiding = false;
startAnimation();
}
void Dropdown::startHide() {
if (_hiding) {
return;
}
_hiding = true;
startAnimation();
}
void Dropdown::startAnimation() {
if (_cache.isNull()) {
showChildren();
_cache = Ui::GrabWidget(this);
}
hideChildren();
_a_appearance.start(
[=] { appearanceCallback(); },
_hiding ? 1. : 0.,
_hiding ? 0. : 1.,
st::defaultInnerDropdown.duration);
}
void Dropdown::appearanceCallback() {
if (!_a_appearance.animating() && _hiding) {
_hiding = false;
hidingFinished();
} else {
update();
}
}
void Dropdown::hidingFinished() {
hide();
_cache = QPixmap();
}
bool Dropdown::eventFilter(QObject *obj, QEvent *e) {
if (e->type() == QEvent::Enter) {
otherEnter();
} else if (e->type() == QEvent::Leave) {
otherLeave();
}
return false;
}
WithDropdownController::WithDropdownController(
not_null<Ui::AbstractButton*> button,
not_null<QWidget*> menuParent,
const style::DropdownMenu &menuSt,
Qt::Alignment menuAlign,
Fn<void(bool)> menuOverCallback)
: _button(button)
, _menuParent(menuParent)
, _menuSt(menuSt)
, _menuAlign(menuAlign)
, _menuOverCallback(std::move(menuOverCallback)) {
button->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter)
|| (e->type() == QEvent::Leave);
}) | rpl::on_next([=](not_null<QEvent*> e) {
_overButton = (e->type() == QEvent::Enter);
if (_overButton) {
InvokeQueued(button, [=] {
if (_overButton) {
showMenu();
}
});
}
}, button->lifetime());
}
not_null<Ui::AbstractButton*> WithDropdownController::button() const {
return _button;
}
Ui::DropdownMenu *WithDropdownController::menu() const {
return _menu.get();
}
void WithDropdownController::updateDropdownGeometry() {
if (!_menu) {
return;
}
const auto bwidth = _button->width();
const auto bheight = _button->height();
const auto mwidth = _menu->width();
const auto mheight = _menu->height();
const auto padding = _menuSt.wrap.padding;
const auto x = st::mediaPlayerMenuPosition.x();
const auto y = st::mediaPlayerMenuPosition.y();
const auto position = _menu->parentWidget()->mapFromGlobal(
_button->mapToGlobal(QPoint())
) + [&] {
switch (_menuAlign) {
case style::al_topleft: return QPoint(
-padding.left() - x,
bheight - padding.top() + y);
case style::al_topright: return QPoint(
bwidth - mwidth + padding.right() + x,
bheight - padding.top() + y);
case style::al_bottomright: return QPoint(
bwidth - mwidth + padding.right() + x,
-mheight + padding.bottom() - y);
case style::al_bottomleft: return QPoint(
-padding.left() - x,
-mheight + padding.bottom() - y);
}
Unexpected("Menu align value.");
}();
_menu->move(position);
}
rpl::producer<bool> WithDropdownController::menuToggledValue() const {
return _menuToggled.value();
}
void WithDropdownController::hideTemporarily() {
if (_menu && !_menu->isHidden()) {
_temporarilyHidden = true;
_menu->hide();
}
}
void WithDropdownController::showBack() {
if (_temporarilyHidden) {
_temporarilyHidden = false;
if (_menu && _menu->isHidden()) {
_menu->show();
}
}
}
void WithDropdownController::showMenu() {
if (_menu) {
return;
}
_menu.emplace(_menuParent, _menuSt);
const auto raw = _menu.get();
_menu->events(
) | rpl::on_next([this](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::Enter) {
_menuOverCallback(true);
} else if (type == QEvent::Leave) {
_menuOverCallback(false);
}
}, _menu->lifetime());
_menu->setHiddenCallback([=]{
if (_menu.get() == raw) {
_menuToggled = false;
}
Ui::PostponeCall(raw, [this] {
_menu = nullptr;
_menuToggled = false;
});
});
_menu->setShowStartCallback([=] {
_menuToggled = true;
});
_menu->setHideStartCallback([=] {
_menuToggled = false;
});
_button->installEventFilter(raw);
fillMenu(raw);
updateDropdownGeometry();
const auto origin = [&] {
using Origin = Ui::PanelAnimation::Origin;
switch (_menuAlign) {
case style::al_topleft: return Origin::TopLeft;
case style::al_topright: return Origin::TopRight;
case style::al_bottomright: return Origin::BottomRight;
case style::al_bottomleft: return Origin::BottomLeft;
}
Unexpected("Menu align value.");
}();
_menu->showAnimated(origin);
_menuToggled = true;
}
OrderController::OrderController(
not_null<Ui::IconButton*> button,
not_null<QWidget*> menuParent,
Fn<void(bool)> menuOverCallback,
rpl::producer<OrderMode> value,
Fn<void(OrderMode)> change)
: WithDropdownController(
button,
menuParent,
st::mediaPlayerMenu,
style::al_topright,
std::move(menuOverCallback))
, _button(button)
, _appOrder(std::move(value))
, _change(std::move(change)) {
button->setClickedCallback([=] {
showMenu();
});
_appOrder.value(
) | rpl::on_next([=] {
updateIcon();
}, button->lifetime());
}
void OrderController::fillMenu(not_null<Ui::DropdownMenu*> menu) {
const auto addOrderAction = [&](OrderMode mode) {
struct Fields {
QString label;
const style::icon &icon;
const style::icon &activeIcon;
};
const auto active = (_appOrder.current() == mode);
const auto callback = [change = _change, mode, active] {
change(active ? OrderMode::Default : mode);
};
const auto fields = [&]() -> Fields {
switch (mode) {
case OrderMode::Reverse: return {
.label = tr::lng_audio_player_reverse(tr::now),
.icon = st::mediaPlayerOrderIconReverse,
.activeIcon = st::mediaPlayerOrderIconReverseActive,
};
case OrderMode::Shuffle: return {
.label = tr::lng_audio_player_shuffle(tr::now),
.icon = st::mediaPlayerOrderIconShuffle,
.activeIcon = st::mediaPlayerOrderIconShuffleActive,
};
}
Unexpected("Order mode in addOrderAction.");
}();
menu->addAction(base::make_unique_q<Ui::Menu::Action>(
menu,
(active
? st::mediaPlayerOrderMenuActive
: st::mediaPlayerOrderMenu),
Ui::Menu::CreateAction(menu, fields.label, callback),
&(active ? fields.activeIcon : fields.icon),
&(active ? fields.activeIcon : fields.icon)));
};
addOrderAction(OrderMode::Reverse);
addOrderAction(OrderMode::Shuffle);
}
void OrderController::updateIcon() {
switch (_appOrder.current()) {
case OrderMode::Default:
_button->setIconOverride(
&st::mediaPlayerReverseDisabledIcon,
&st::mediaPlayerReverseDisabledIconOver);
_button->setRippleColorOverride(
&st::mediaPlayerRepeatDisabledRippleBg);
break;
case OrderMode::Reverse:
_button->setIconOverride(&st::mediaPlayerReverseIcon);
_button->setRippleColorOverride(nullptr);
break;
case OrderMode::Shuffle:
_button->setIconOverride(&st::mediaPlayerShuffleIcon);
_button->setRippleColorOverride(nullptr);
break;
}
}
SpeedController::SpeedController(
not_null<Ui::AbstractButton*> button,
const style::MediaSpeedButton &st,
not_null<QWidget*> menuParent,
Fn<void(bool)> menuOverCallback,
Fn<float64(bool lastNonDefault)> value,
Fn<void(float64)> change,
std::vector<int> qualities,
Fn<VideoQuality()> quality,
Fn<void(int)> changeQuality)
: WithDropdownController(
button,
menuParent,
st.menu.dropdown,
st.menuAlign,
std::move(menuOverCallback))
, _st(st)
, _lookup(std::move(value))
, _change(std::move(change))
, _qualities(std::move(qualities))
, _lookupQuality(std::move(quality))
, _changeQuality(std::move(changeQuality)) {
Expects(_qualities.empty() || (_lookupQuality && _changeQuality));
button->setClickedCallback([=] {
if (_lookup && !_lookupQuality && !_changeQuality) {
toggleDefault();
save();
if (const auto current = menu()) {
current->otherEnter();
}
} else {
showMenu();
}
});
if (const auto lookup = _lookup) {
setSpeed(lookup(false));
_speed = lookup(true);
}
}
rpl::producer<> SpeedController::saved() const {
return _saved.events();
}
rpl::producer<float64> SpeedController::realtimeValue() const {
return _speedChanged.events_starting_with(speed());
}
float64 SpeedController::speed() const {
return _isDefault ? 1. : _speed;
}
bool SpeedController::isDefault() const {
return _isDefault;
}
float64 SpeedController::lastNonDefaultSpeed() const {
return _speed;
}
void SpeedController::toggleDefault() {
_isDefault = !_isDefault;
_speedChanged.fire(speed());
}
void SpeedController::setSpeed(float64 newSpeed) {
if (!(_isDefault = EqualSpeeds(newSpeed, 1.))) {
_speed = newSpeed;
}
_speedChanged.fire(speed());
}
void SpeedController::save() {
if (const auto change = _change) {
change(speed());
}
_saved.fire({});
}
void SpeedController::setQuality(VideoQuality quality) {
_quality = quality;
_changeQuality(quality.manual ? quality.height : 0);
}
void SpeedController::fillMenu(not_null<Ui::DropdownMenu*> menu) {
if (_lookup) {
FillSpeedMenu(
menu->menu(),
_st.menu,
_speedChanged.events_starting_with(speed()),
[=](float64 speed) { setSpeed(speed); save(); },
!_qualities.empty());
}
if (_qualities.empty()) {
return;
}
_quality = _lookupQuality();
const auto raw = menu->menu();
const auto &st = _st.menu;
if (_lookup) {
raw->addSeparator(&st.dropdown.menu.separator);
}
const auto add = [&](int quality) {
const auto automatic = tr::lng_mediaview_quality_auto(tr::now);
const auto text = quality ? u"%1p"_q.arg(quality) : automatic;
auto action = base::make_unique_q<Ui::Menu::Action>(
raw,
st.qualityMenu,
Ui::Menu::CreateAction(
raw,
text,
[=] { _changeQuality(quality); }),
nullptr,
nullptr);
const auto raw = action.get();
const auto check = Ui::CreateChild<Ui::RpWidget>(raw);
check->resize(st.activeCheck.size());
check->paintRequest(
) | rpl::on_next([check, icon = &st.activeCheck] {
auto p = QPainter(check);
icon->paint(p, 0, 0, check->width());
}, check->lifetime());
raw->sizeValue(
) | rpl::on_next([=, skip = st.activeCheckSkip](QSize size) {
check->moveToRight(
skip,
(size.height() - check->height()) / 2,
size.width());
}, check->lifetime());
check->setAttribute(Qt::WA_TransparentForMouseEvents);
_quality.value(
) | rpl::on_next([=](VideoQuality now) {
const auto chosen = now.manual
? (now.height == quality)
: !quality;
raw->action()->setEnabled(!chosen);
if (!quality) {
raw->action()->setText(automatic
+ (now.manual ? QString() : u"\t%1p"_q.arg(now.height)));
}
check->setVisible(chosen);
}, raw->lifetime());
menu->addAction(std::move(action));
};
add(0);
for (const auto quality : _qualities) {
add(quality);
}
}
} // namespace Media::Player

View File

@@ -0,0 +1,167 @@
/*
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
#include "base/timer.h"
#include "media/media_common.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace style {
struct MediaSpeedMenu;
struct MediaSpeedButton;
struct DropdownMenu;
} // namespace style
namespace Ui {
class DropdownMenu;
class AbstractButton;
class IconButton;
} // namespace Ui
namespace Ui::Menu {
class Menu;
} // namespace Ui::Menu
namespace Media::Player {
class Dropdown final : public Ui::RpWidget {
public:
explicit Dropdown(QWidget *parent);
bool overlaps(const QRect &globalRect);
QMargins getMargin() const;
protected:
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
bool eventFilter(QObject *obj, QEvent *e) override;
private:
void startHide();
void startShow();
void otherEnter();
void otherLeave();
void appearanceCallback();
void hidingFinished();
void startAnimation();
bool _hiding = false;
QPixmap _cache;
Ui::Animations::Simple _a_appearance;
base::Timer _hideTimer;
base::Timer _showTimer;
};
class WithDropdownController {
public:
WithDropdownController(
not_null<Ui::AbstractButton*> button,
not_null<QWidget*> menuParent,
const style::DropdownMenu &menuSt,
Qt::Alignment menuAlign,
Fn<void(bool)> menuOverCallback);
virtual ~WithDropdownController() = default;
[[nodiscard]] not_null<Ui::AbstractButton*> button() const;
Ui::DropdownMenu *menu() const;
void updateDropdownGeometry();
[[nodiscard]] rpl::producer<bool> menuToggledValue() const;
void hideTemporarily();
void showBack();
protected:
void showMenu();
private:
virtual void fillMenu(not_null<Ui::DropdownMenu*> menu) = 0;
const not_null<Ui::AbstractButton*> _button;
const not_null<QWidget*> _menuParent;
const style::DropdownMenu &_menuSt;
const Qt::Alignment _menuAlign = Qt::AlignTop | Qt::AlignRight;
const Fn<void(bool)> _menuOverCallback;
base::unique_qptr<Ui::DropdownMenu> _menu;
rpl::variable<bool> _menuToggled;
bool _temporarilyHidden = false;
bool _overButton = false;
};
class OrderController final : public WithDropdownController {
public:
OrderController(
not_null<Ui::IconButton*> button,
not_null<QWidget*> menuParent,
Fn<void(bool)> menuOverCallback,
rpl::producer<OrderMode> value,
Fn<void(OrderMode)> change);
private:
void fillMenu(not_null<Ui::DropdownMenu*> menu) override;
void updateIcon();
const not_null<Ui::IconButton*> _button;
rpl::variable<OrderMode> _appOrder;
Fn<void(OrderMode)> _change;
};
class SpeedController final : public WithDropdownController {
public:
SpeedController(
not_null<Ui::AbstractButton*> button,
const style::MediaSpeedButton &st,
not_null<QWidget*> menuParent,
Fn<void(bool)> menuOverCallback,
Fn<float64(bool lastNonDefault)> value,
Fn<void(float64)> change,
std::vector<int> qualities = {},
Fn<VideoQuality()> quality = nullptr,
Fn<void(int)> changeQuality = nullptr);
[[nodiscard]] rpl::producer<> saved() const;
[[nodiscard]] rpl::producer<float64> realtimeValue() const;
private:
void fillMenu(not_null<Ui::DropdownMenu*> menu) override;
[[nodiscard]] float64 speed() const;
[[nodiscard]] bool isDefault() const;
[[nodiscard]] float64 lastNonDefaultSpeed() const;
void toggleDefault();
void setSpeed(float64 newSpeed);
void setQuality(VideoQuality quality);
void save();
const style::MediaSpeedButton &_st;
Fn<float64(bool lastNonDefault)> _lookup;
Fn<void(float64)> _change;
float64 _speed = kSpedUpDefault;
bool _isDefault = true;
rpl::event_stream<float64> _speedChanged;
rpl::event_stream<> _saved;
std::vector<int> _qualities;
Fn<VideoQuality()> _lookupQuality;
Fn<void(int)> _changeQuality;
rpl::variable<VideoQuality> _quality;
};
} // namespace Media::Player

View File

@@ -0,0 +1,667 @@
/*
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/player/media_player_float.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "history/view/media/history_view_media.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "media/audio/media_audio.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/view/media_view_playback_progress.h"
#include "media/player/media_player_instance.h"
#include "window/window_session_controller.h"
#include "window/section_widget.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_media_player.h"
#include "styles/style_chat.h"
#include <QtWidgets/QApplication>
namespace Media {
namespace Player {
using DoubleClickedCallback = Fn<void(not_null<const HistoryItem*>)>;
RoundPainter::RoundPainter(not_null<HistoryItem*> item)
: _item(item) {
}
bool RoundPainter::fillFrame(const QSize &size) {
auto creating = _frame.isNull();
const auto ratio = style::DevicePixelRatio();
if (creating) {
_frame = QImage(
size * ratio,
QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
}
auto frameInner = [&] {
return QRect(QPoint(), _frame.size() / ratio);
};
if (const auto streamed = instance()->roundVideoStreamed(_item)) {
auto request = Streaming::FrameRequest::NonStrict();
request.outer = request.resize = _frame.size();
if (_roundingMask.size() != request.outer) {
_roundingMask = Images::EllipseMask(frameInner().size());
}
request.mask = _roundingMask;
auto frame = streamed->frame(request);
if (!frame.isNull()) {
_frame.fill(Qt::transparent);
auto p = QPainter(&_frame);
PainterHighQualityEnabler hq(p);
p.drawImage(frameInner(), frame);
return true;
}
}
if (creating) {
_frame.fill(Qt::transparent);
auto p = QPainter(&_frame);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::imageBg);
p.drawEllipse(frameInner());
}
return false;
}
const QImage &RoundPainter::frame() const {
return _frame;
}
Float::Float(
QWidget *parent,
not_null<HistoryItem*> item,
Fn<void(bool visible)> toggleCallback,
Fn<void(bool closed)> draggedCallback,
DoubleClickedCallback doubleClickedCallback)
: RpWidget(parent)
, _item(item)
, _toggleCallback(std::move(toggleCallback))
, _draggedCallback(std::move(draggedCallback))
, _doubleClickedCallback(std::move(doubleClickedCallback)) {
auto media = _item->media();
Assert(media != nullptr);
auto document = media->document();
Assert(document != nullptr);
Assert(document->isVideoMessage());
auto margin = st::mediaPlayerFloatMargin;
auto size = 2 * margin + st::mediaPlayerFloatSize;
resize(size, size);
_roundPainter = std::make_unique<RoundPainter>(item);
prepareShadow();
document->session().data().itemRepaintRequest(
) | rpl::on_next([this](auto item) {
if (_item == item) {
repaintItem();
}
}, lifetime());
document->session().data().itemRemoved(
) | rpl::on_next([this](auto item) {
if (_item == item) {
detach();
}
}, lifetime());
document->session().account().sessionChanges(
) | rpl::on_next([=] {
detach();
}, lifetime());
setCursor(style::cur_pointer);
}
void Float::mousePressEvent(QMouseEvent *e) {
_down = true;
_downPoint = e->pos();
}
void Float::mouseMoveEvent(QMouseEvent *e) {
if (_down && (e->pos() - _downPoint).manhattanLength() > QApplication::startDragDistance()) {
_down = false;
_drag = true;
_dragLocalPoint = e->pos();
} else if (_drag) {
auto delta = (e->pos() - _dragLocalPoint);
move(pos() + delta);
setOpacity(outRatio());
}
}
float64 Float::outRatio() const {
auto parent = parentWidget()->rect();
auto min = 1.;
if (x() < parent.x()) {
accumulate_min(min, 1. - (parent.x() - x()) / float64(width()));
}
if (y() < parent.y()) {
accumulate_min(min, 1. - (parent.y() - y()) / float64(height()));
}
if (x() + width() > parent.x() + parent.width()) {
accumulate_min(min, 1. - (x() + width() - parent.x() - parent.width()) / float64(width()));
}
if (y() + height() > parent.y() + parent.height()) {
accumulate_min(min, 1. - (y() + height() - parent.y() - parent.height()) / float64(height()));
}
return std::clamp(min, 0., 1.);
}
void Float::mouseReleaseEvent(QMouseEvent *e) {
if (base::take(_down) && _item) {
pauseResume();
}
if (_drag) {
finishDrag(outRatio() < 0.5);
}
}
void Float::finishDrag(bool closed) {
_drag = false;
if (_draggedCallback) {
_draggedCallback(closed);
}
}
void Float::mouseDoubleClickEvent(QMouseEvent *e) {
if (_item && _doubleClickedCallback) {
// Handle second click.
pauseResume();
_doubleClickedCallback(_item);
}
}
void Float::pauseResume() {
if (const auto streamed = getStreamed()) {
if (streamed->paused()) {
streamed->resume();
} else {
streamed->pause();
}
}
}
void Float::detach() {
if (_item) {
_item = nullptr;
_roundPainter = nullptr;
if (_toggleCallback) {
_toggleCallback(false);
}
}
}
void Float::prepareShadow() {
const auto ratio = style::DevicePixelRatio();
auto shadow = QImage(
size() * ratio,
QImage::Format_ARGB32_Premultiplied);
shadow.fill(Qt::transparent);
shadow.setDevicePixelRatio(ratio);
{
auto p = QPainter(&shadow);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::shadowFg);
auto extend = 2 * st::lineWidth;
p.drawEllipse(getInnerRect().marginsAdded(QMargins(extend, extend, extend, extend)));
}
_shadow = Ui::PixmapFromImage(Images::Blur(std::move(shadow)));
}
QRect Float::getInnerRect() const {
auto margin = st::mediaPlayerFloatMargin;
return rect().marginsRemoved(QMargins(margin, margin, margin, margin));
}
void Float::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
p.setOpacity(_opacity);
p.drawPixmap(0, 0, _shadow);
const auto inner = getInnerRect();
if (!(_roundPainter && _roundPainter->fillFrame(inner.size()))
&& _toggleCallback) {
_toggleCallback(false);
}
if (_roundPainter) {
p.drawImage(inner.topLeft(), _roundPainter->frame());
}
const auto playback = getPlayback();
const auto progress = playback ? playback->value() : 1.;
if (progress > 0.) {
auto pen = st::historyVideoMessageProgressFg->p;
//auto was = p.pen();
pen.setWidth(st::radialLine);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.setOpacity(_opacity * st::historyVideoMessageProgressOpacity);
auto from = arc::kQuarterLength;
auto len = -qRound(arc::kFullLength * progress);
auto stepInside = st::radialLine / 2;
{
PainterHighQualityEnabler hq(p);
p.drawArc(inner.marginsRemoved(QMargins(stepInside, stepInside, stepInside, stepInside)), from, len);
}
//p.setPen(was);
//p.setOpacity(_opacity);
}
}
Streaming::Instance *Float::getStreamed() const {
return instance()->roundVideoStreamed(_item);
}
View::PlaybackProgress *Float::getPlayback() const {
return instance()->roundVideoPlayback(_item);
}
bool Float::hasFrame() const {
return (getStreamed() != nullptr);
}
void Float::repaintItem() {
update();
if (hasFrame() && _toggleCallback) {
_toggleCallback(true);
}
}
template <typename ToggleCallback, typename DraggedCallback>
FloatController::Item::Item(
not_null<QWidget*> parent,
not_null<HistoryItem*> item,
ToggleCallback toggle,
DraggedCallback dragged,
DoubleClickedCallback doubleClicked)
: animationSide(RectPart::Right)
, column(Window::Column::Second)
, corner(RectPart::TopRight)
, widget(
parent,
item,
[=, toggle = std::move(toggle)](bool visible) {
toggle(this, visible);
},
[=, dragged = std::move(dragged)](bool closed) {
dragged(this, closed);
},
std::move(doubleClicked)) {
}
FloatController::FloatController(not_null<FloatDelegate*> delegate)
: _delegate(delegate)
, _parent(_delegate->floatPlayerWidget()) {
Media::Player::instance()->trackChanged(
) | rpl::filter([=](AudioMsgId::Type type) {
return (type == AudioMsgId::Type::Voice);
}) | rpl::on_next([=] {
checkCurrent();
}, _lifetime);
startDelegateHandling();
}
void FloatController::replaceDelegate(not_null<FloatDelegate*> delegate) {
_delegateLifetime.destroy();
_delegate = delegate;
_parent = _delegate->floatPlayerWidget();
startDelegateHandling();
for (const auto &item : _items) {
item->widget->setParent(_parent);
}
checkVisibility();
}
void FloatController::startDelegateHandling() {
_delegate->floatPlayerCheckVisibilityRequests(
) | rpl::on_next([=] {
checkVisibility();
}, _delegateLifetime);
_delegate->floatPlayerHideAllRequests(
) | rpl::on_next([=] {
hideAll();
}, _delegateLifetime);
_delegate->floatPlayerShowVisibleRequests(
) | rpl::on_next([=] {
showVisible();
}, _delegateLifetime);
_delegate->floatPlayerRaiseAllRequests(
) | rpl::on_next([=] {
raiseAll();
}, _delegateLifetime);
_delegate->floatPlayerUpdatePositionsRequests(
) | rpl::on_next([=] {
updatePositions();
}, _delegateLifetime);
_delegate->floatPlayerFilterWheelEventRequests(
) | rpl::on_next([=](
const FloatDelegate::FloatPlayerFilterWheelEventRequest &request) {
*request.result = filterWheelEvent(request.object, request.event);
}, _delegateLifetime);
_delegate->floatPlayerAreaUpdates(
) | rpl::on_next([=] {
checkVisibility();
}, _delegateLifetime);
}
void FloatController::checkCurrent() {
const auto state = Media::Player::instance()->current(AudioMsgId::Type::Voice);
const auto audio = state.audio();
const auto fullId = state.contextId();
const auto last = current();
if (last
&& audio
&& !last->widget->detached()
&& (&last->widget->item()->history()->session() == &audio->session())
&& (last->widget->item()->fullId() == fullId)) {
return;
}
if (last) {
last->widget->detach();
}
if (!audio) {
return;
}
if (const auto item = audio->session().data().message(fullId)) {
if (const auto media = item->media()) {
if (const auto document = media->document()) {
if (document->isVideoMessage()) {
create(item);
}
}
}
}
}
void FloatController::create(not_null<HistoryItem*> item) {
_items.push_back(std::make_unique<Item>(
_parent,
item,
[=](not_null<Item*> instance, bool visible) {
instance->hiddenByWidget = !visible;
toggle(instance);
},
[=](not_null<Item*> instance, bool closed) {
finishDrag(instance, closed);
},
[=](not_null<const HistoryItem*> item) {
_delegate->floatPlayerDoubleClickEvent(item);
}));
current()->column = Core::App().settings().floatPlayerColumn();
current()->corner = Core::App().settings().floatPlayerCorner();
checkVisibility();
}
void FloatController::toggle(not_null<Item*> instance) {
auto visible = !instance->hiddenByHistory && !instance->hiddenByWidget && instance->widget->isReady();
if (instance->visible != visible) {
instance->widget->resetMouseState();
instance->visible = visible;
if (!instance->visibleAnimation.animating() && !instance->hiddenByDrag) {
auto finalRect = QRect(getPosition(instance), instance->widget->size());
instance->animationSide = getSide(finalRect.center());
}
instance->visibleAnimation.start([=] {
updatePosition(instance);
}, visible ? 0. : 1., visible ? 1. : 0., st::slideDuration, visible ? anim::easeOutCirc : anim::linear);
updatePosition(instance);
}
}
void FloatController::checkVisibility() {
const auto instance = current();
if (!instance) {
return;
}
const auto item = instance->widget->item();
instance->hiddenByHistory = item
? _delegate->floatPlayerIsVisible(item)
: false;
toggle(instance);
updatePosition(instance);
}
void FloatController::hideAll() {
for (const auto &instance : _items) {
instance->widget->hide();
}
}
void FloatController::showVisible() {
for (const auto &instance : _items) {
if (instance->visible) {
instance->widget->show();
}
}
}
void FloatController::raiseAll() {
for (const auto &instance : _items) {
instance->widget->raise();
}
}
void FloatController::updatePositions() {
for (const auto &instance : _items) {
updatePosition(instance.get());
}
}
std::optional<bool> FloatController::filterWheelEvent(
not_null<QObject*> object,
not_null<QEvent*> event) {
for (const auto &instance : _items) {
if (instance->widget == object) {
const auto section = _delegate->floatPlayerGetSection(
instance->column);
return section->floatPlayerHandleWheelEvent(event);
}
}
return std::nullopt;
}
void FloatController::updatePosition(not_null<Item*> instance) {
auto visible = instance->visibleAnimation.value(instance->visible ? 1. : 0.);
if (visible == 0. && !instance->visible) {
instance->widget->hide();
if (instance->widget->detached()) {
InvokeQueued(instance->widget, [=] {
remove(instance);
});
}
return;
}
if (!instance->widget->dragged()) {
if (instance->widget->isHidden()) {
instance->widget->show();
}
auto dragged = instance->draggedAnimation.value(1.);
auto position = QPoint();
if (instance->hiddenByDrag) {
instance->widget->setOpacity(instance->widget->countOpacityByParent());
position = getHiddenPosition(instance->dragFrom, instance->widget->size(), instance->animationSide);
} else {
instance->widget->setOpacity(visible * visible);
position = getPosition(instance);
if (visible < 1.) {
auto hiddenPosition = getHiddenPosition(position, instance->widget->size(), instance->animationSide);
position.setX(anim::interpolate(hiddenPosition.x(), position.x(), visible));
position.setY(anim::interpolate(hiddenPosition.y(), position.y(), visible));
}
}
if (dragged < 1.) {
position.setX(anim::interpolate(instance->dragFrom.x(), position.x(), dragged));
position.setY(anim::interpolate(instance->dragFrom.y(), position.y(), dragged));
}
instance->widget->move(position);
}
}
QPoint FloatController::getHiddenPosition(
QPoint position,
QSize size,
RectPart side) const {
switch (side) {
case RectPart::Left: return { -size.width(), position.y() };
case RectPart::Top: return { position.x(), -size.height() };
case RectPart::Right: return { _parent->width(), position.y() };
case RectPart::Bottom: return { position.x(), _parent->height() };
}
Unexpected("Bad side in MainWidget::getFloatPlayerHiddenPosition().");
}
QPoint FloatController::getPosition(not_null<Item*> instance) const {
const auto section = _delegate->floatPlayerGetSection(instance->column);
const auto rect = section->floatPlayerAvailableRect();
auto position = rect.topLeft();
if (IsBottomCorner(instance->corner)) {
position.setY(position.y() + rect.height() - instance->widget->height());
}
if (IsRightCorner(instance->corner)) {
position.setX(position.x() + rect.width() - instance->widget->width());
}
return _parent->mapFromGlobal(position);
}
RectPart FloatController::getSide(QPoint center) const {
const auto left = std::abs(center.x());
const auto right = std::abs(_parent->width() - center.x());
const auto top = std::abs(center.y());
const auto bottom = std::abs(_parent->height() - center.y());
if (left < right && left < top && left < bottom) {
return RectPart::Left;
} else if (right < top && right < bottom) {
return RectPart::Right;
} else if (top < bottom) {
return RectPart::Top;
}
return RectPart::Bottom;
}
void FloatController::remove(not_null<Item*> instance) {
auto widget = std::move(instance->widget);
auto i = ranges::find_if(_items, [&](auto &item) {
return (item.get() == instance);
});
Assert(i != _items.end());
_items.erase(i);
// ~QWidget() can call HistoryInner::enterEvent() which can
// lead to repaintHistoryItem() and we'll have an instance
// in _items with destroyed widget. So we destroy the
// instance first and only after that destroy the widget.
widget.destroy();
}
void FloatController::updateColumnCorner(QPoint center) {
Expects(!_items.empty());
auto size = _items.back()->widget->size();
auto min = INT_MAX;
auto column = Core::App().settings().floatPlayerColumn();
auto corner = Core::App().settings().floatPlayerCorner();
auto checkSection = [&](
not_null<FloatSectionDelegate*> widget,
Window::Column widgetColumn) {
auto rect = _parent->mapFromGlobal(
widget->floatPlayerAvailableRect());
auto left = rect.x() + (size.width() / 2);
auto right = rect.x() + rect.width() - (size.width() / 2);
auto top = rect.y() + (size.height() / 2);
auto bottom = rect.y() + rect.height() - (size.height() / 2);
auto checkCorner = [&](QPoint point, RectPart checked) {
auto distance = (point - center).manhattanLength();
if (min > distance) {
min = distance;
column = widgetColumn;
corner = checked;
}
};
checkCorner({ left, top }, RectPart::TopLeft);
checkCorner({ right, top }, RectPart::TopRight);
checkCorner({ left, bottom }, RectPart::BottomLeft);
checkCorner({ right, bottom }, RectPart::BottomRight);
};
_delegate->floatPlayerEnumerateSections(checkSection);
auto &settings = Core::App().settings();
if (settings.floatPlayerColumn() != column) {
settings.setFloatPlayerColumn(column);
Core::App().saveSettingsDelayed();
}
if (settings.floatPlayerCorner() != corner) {
settings.setFloatPlayerCorner(corner);
Core::App().saveSettingsDelayed();
}
}
void FloatController::finishDrag(not_null<Item*> instance, bool closed) {
instance->dragFrom = instance->widget->pos();
const auto center = instance->widget->geometry().center();
if (closed) {
instance->hiddenByDrag = true;
instance->animationSide = getSide(center);
}
updateColumnCorner(center);
instance->column = Core::App().settings().floatPlayerColumn();
instance->corner = Core::App().settings().floatPlayerCorner();
instance->draggedAnimation.stop();
instance->draggedAnimation.start(
[=] { updatePosition(instance); },
0.,
1.,
st::slideDuration,
anim::sineInOut);
updatePosition(instance);
if (closed) {
if (const auto item = instance->widget->item()) {
_closeEvents.fire(item->fullId());
}
instance->widget->detach();
Media::Player::instance()->stop(AudioMsgId::Type::Voice);
}
}
} // namespace Player
} // namespace Media

View File

@@ -0,0 +1,287 @@
/*
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
#include "base/object_ptr.h"
#include "ui/effects/animations.h"
#include "ui/rect_part.h"
#include "ui/rp_widget.h"
namespace Window {
class SessionController;
enum class Column;
} // namespace Window
namespace Media {
namespace View {
class PlaybackProgress;
} // namespace View
} // namespace Media
namespace Media {
namespace Streaming {
class Instance;
} // namespace Streaming
} // namespace Media
namespace Media {
namespace Player {
class RoundPainter {
public:
RoundPainter(not_null<HistoryItem*> item);
bool fillFrame(const QSize &size);
const QImage &frame() const;
private:
const not_null<HistoryItem*> _item;
QImage _roundingMask;
QImage _frame;
};
class Float final : public Ui::RpWidget {
public:
Float(
QWidget *parent,
not_null<HistoryItem*> item,
Fn<void(bool visible)> toggleCallback,
Fn<void(bool closed)> draggedCallback,
Fn<void(not_null<const HistoryItem*>)> doubleClickedCallback);
[[nodiscard]] HistoryItem *item() const {
return _item;
}
void setOpacity(float64 opacity) {
if (_opacity != opacity) {
_opacity = opacity;
update();
}
}
[[nodiscard]] float64 countOpacityByParent() const {
return outRatio();
}
[[nodiscard]] bool isReady() const {
return (getStreamed() != nullptr);
}
void detach();
[[nodiscard]] bool detached() const {
return !_item;
}
[[nodiscard]] bool dragged() const {
return _drag;
}
void resetMouseState() {
_down = false;
if (_drag) {
finishDrag(false);
}
}
protected:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
private:
[[nodiscard]] float64 outRatio() const;
[[nodiscard]] Streaming::Instance *getStreamed() const;
[[nodiscard]] View::PlaybackProgress *getPlayback() const;
void repaintItem();
void prepareShadow();
bool hasFrame() const;
[[nodiscard]] QRect getInnerRect() const;
void finishDrag(bool closed);
void pauseResume();
HistoryItem *_item = nullptr;
Fn<void(bool visible)> _toggleCallback;
std::unique_ptr<RoundPainter> _roundPainter;
float64 _opacity = 1.;
QPixmap _shadow;
bool _down = false;
QPoint _downPoint;
bool _drag = false;
QPoint _dragLocalPoint;
Fn<void(bool closed)> _draggedCallback;
Fn<void(not_null<const HistoryItem*>)> _doubleClickedCallback;
};
class FloatSectionDelegate {
public:
virtual QRect floatPlayerAvailableRect() = 0;
virtual bool floatPlayerHandleWheelEvent(QEvent *e) = 0;
};
class FloatDelegate {
public:
virtual not_null<Ui::RpWidget*> floatPlayerWidget() = 0;
virtual void floatPlayerToggleGifsPaused(bool paused) = 0;
virtual not_null<FloatSectionDelegate*> floatPlayerGetSection(
Window::Column column) = 0;
virtual void floatPlayerEnumerateSections(Fn<void(
not_null<FloatSectionDelegate*> widget,
Window::Column widgetColumn)> callback) = 0;
virtual bool floatPlayerIsVisible(not_null<HistoryItem*> item) = 0;
virtual rpl::producer<> floatPlayerCheckVisibilityRequests() {
return _checkVisibility.events();
}
virtual rpl::producer<> floatPlayerHideAllRequests() {
return _hideAll.events();
}
virtual rpl::producer<> floatPlayerShowVisibleRequests() {
return _showVisible.events();
}
virtual rpl::producer<> floatPlayerRaiseAllRequests() {
return _raiseAll.events();
}
virtual rpl::producer<> floatPlayerUpdatePositionsRequests() {
return _updatePositions.events();
}
virtual rpl::producer<> floatPlayerAreaUpdates() {
return _areaUpdates.events();
}
virtual void floatPlayerDoubleClickEvent(
not_null<const HistoryItem*> item) {
}
struct FloatPlayerFilterWheelEventRequest {
not_null<QObject*> object;
not_null<QEvent*> event;
not_null<std::optional<bool>*> result;
};
virtual auto floatPlayerFilterWheelEventRequests()
-> rpl::producer<FloatPlayerFilterWheelEventRequest> {
return _filterWheelEvent.events();
}
virtual ~FloatDelegate() = default;
protected:
void floatPlayerCheckVisibility() {
_checkVisibility.fire({});
}
void floatPlayerHideAll() {
_hideAll.fire({});
}
void floatPlayerShowVisible() {
_showVisible.fire({});
}
void floatPlayerRaiseAll() {
_raiseAll.fire({});
}
void floatPlayerUpdatePositions() {
_updatePositions.fire({});
}
void floatPlayerAreaUpdated() {
_areaUpdates.fire({});
}
std::optional<bool> floatPlayerFilterWheelEvent(
not_null<QObject*> object,
not_null<QEvent*> event) {
auto result = std::optional<bool>();
_filterWheelEvent.fire({ object, event, &result });
return result;
}
private:
rpl::event_stream<> _checkVisibility;
rpl::event_stream<> _hideAll;
rpl::event_stream<> _showVisible;
rpl::event_stream<> _raiseAll;
rpl::event_stream<> _updatePositions;
rpl::event_stream<> _areaUpdates;
rpl::event_stream<FloatPlayerFilterWheelEventRequest> _filterWheelEvent;
};
class FloatController final {
public:
explicit FloatController(not_null<FloatDelegate*> delegate);
void replaceDelegate(not_null<FloatDelegate*> delegate);
[[nodiscard]] rpl::producer<FullMsgId> closeEvents() const {
return _closeEvents.events();
}
private:
struct Item {
template <typename ToggleCallback, typename DraggedCallback>
Item(
not_null<QWidget*> parent,
not_null<HistoryItem*> item,
ToggleCallback toggle,
DraggedCallback dragged,
Fn<void(not_null<const HistoryItem*>)> doubleClicked);
bool hiddenByWidget = false;
bool hiddenByHistory = false;
bool visible = false;
RectPart animationSide;
Ui::Animations::Simple visibleAnimation;
Window::Column column;
RectPart corner;
QPoint dragFrom;
Ui::Animations::Simple draggedAnimation;
bool hiddenByDrag = false;
object_ptr<Float> widget;
};
void checkCurrent();
void create(not_null<HistoryItem*> item);
void toggle(not_null<Item*> instance);
void updatePosition(not_null<Item*> instance);
void remove(not_null<Item*> instance);
Item *current() const {
return _items.empty() ? nullptr : _items.back().get();
}
void finishDrag(
not_null<Item*> instance,
bool closed);
void updateColumnCorner(QPoint center);
QPoint getPosition(not_null<Item*> instance) const;
QPoint getHiddenPosition(
QPoint position,
QSize size,
RectPart side) const;
RectPart getSide(QPoint center) const;
void startDelegateHandling();
void checkVisibility();
void hideAll();
void showVisible();
void raiseAll();
void updatePositions();
std::optional<bool> filterWheelEvent(
not_null<QObject*> object,
not_null<QEvent*> event);
not_null<FloatDelegate*> _delegate;
not_null<Ui::RpWidget*> _parent;
std::vector<std::unique_ptr<Item>> _items;
rpl::event_stream<FullMsgId> _closeEvents;
rpl::lifetime _delegateLifetime;
rpl::lifetime _lifetime;
};
} // namespace Player
} // namespace Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,321 @@
/*
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
#include "data/data_audio_msg_id.h"
#include "data/data_shared_media.h"
class AudioMsgId;
class DocumentData;
class History;
namespace Media {
enum class RepeatMode;
enum class OrderMode;
} // namespace Media
namespace Media {
namespace Audio {
class Instance;
} // namespace Audio
} // namespace Media
namespace Media {
namespace View {
class PlaybackProgress;
} // namespace View
} // namespace Media
namespace Media {
namespace Streaming {
class Document;
class Instance;
struct PlaybackOptions;
struct Update;
enum class Error;
} // namespace Streaming
} // namespace Media
namespace base {
class PowerSaveBlocker;
} // namespace base
namespace Media {
namespace Player {
extern const char kOptionDisableAutoplayNext[];
class Instance;
struct TrackState;
void start(not_null<Audio::Instance*> instance);
void finish(not_null<Audio::Instance*> instance);
void SaveLastPlaybackPosition(
not_null<DocumentData*> document,
const TrackState &state);
not_null<Instance*> instance();
class Instance final {
public:
enum class Seeking {
Start,
Finish,
Cancel,
};
void play(AudioMsgId::Type type);
void pause(AudioMsgId::Type type);
void stop(AudioMsgId::Type type, bool asFinished = false);
void playPause(AudioMsgId::Type type);
bool next(AudioMsgId::Type type);
bool previous(AudioMsgId::Type type);
AudioMsgId::Type getActiveType() const;
void play() {
play(getActiveType());
}
void pause() {
pause(getActiveType());
}
void stop() {
stop(getActiveType());
}
void playPause() {
playPause(getActiveType());
}
bool next() {
return next(getActiveType());
}
bool previous() {
return previous(getActiveType());
}
void playPauseCancelClicked(AudioMsgId::Type type);
void play(const AudioMsgId &audioId);
void playPause(const AudioMsgId &audioId);
[[nodiscard]] TrackState getState(AudioMsgId::Type type) const;
[[nodiscard]] Streaming::Instance *roundVideoStreamed(
HistoryItem *item) const;
[[nodiscard]] View::PlaybackProgress *roundVideoPlayback(
HistoryItem *item) const;
[[nodiscard]] Streaming::Instance *roundVideoPreview(
not_null<DocumentData*> document) const;
[[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const {
if (const auto data = getData(type)) {
return data->current;
}
return AudioMsgId();
}
[[nodiscard]] bool isSeeking(AudioMsgId::Type type) const {
if (const auto data = getData(type)) {
return (data->seeking == data->current);
}
return false;
}
void startSeeking(AudioMsgId::Type type);
void finishSeeking(AudioMsgId::Type type, float64 progress);
void cancelSeeking(AudioMsgId::Type type);
void updateVoicePlaybackSpeed();
[[nodiscard]] bool nextAvailable(AudioMsgId::Type type) const;
[[nodiscard]] bool previousAvailable(AudioMsgId::Type type) const;
struct Switch {
AudioMsgId from;
FullMsgId to;
};
[[nodiscard]] rpl::producer<Switch> switchToNextEvents() const {
return _switchToNext.events();
}
[[nodiscard]] rpl::producer<AudioMsgId::Type> tracksFinished() const {
return _tracksFinished.events();
}
[[nodiscard]] rpl::producer<AudioMsgId::Type> trackChanged() const {
return _trackChanged.events();
}
[[nodiscard]] rpl::producer<> playlistChanges(
AudioMsgId::Type type) const;
[[nodiscard]] rpl::producer<TrackState> updatedNotifier() const {
return _updatedNotifier.events();
}
[[nodiscard]] rpl::producer<> stops(AudioMsgId::Type type) const;
[[nodiscard]] rpl::producer<> startsPlay(AudioMsgId::Type type) const;
[[nodiscard]] rpl::producer<Seeking> seekingChanges(
AudioMsgId::Type type) const;
[[nodiscard]] rpl::producer<> closePlayerRequests() const {
return _closePlayerRequests.events();
}
void stopAndClose();
private:
using SharedMediaType = Storage::SharedMediaType;
using SliceKey = SparseIdsMergedSlice::Key;
struct Streamed;
struct ShuffleData;
struct Data {
Data(AudioMsgId::Type type, SharedMediaType overview);
Data(Data &&other);
Data &operator=(Data &&other);
~Data();
AudioMsgId::Type type;
Storage::SharedMediaType overview;
AudioMsgId current;
AudioMsgId seeking;
std::optional<SparseIdsMergedSlice> playlistSlice;
std::optional<SliceKey> playlistSliceKey;
std::optional<SliceKey> playlistRequestedKey;
std::optional<SparseIdsMergedSlice> playlistOtherSlice;
std::optional<SliceKey> playlistOtherRequestedKey;
std::optional<int> playlistIndex;
rpl::lifetime playlistLifetime;
rpl::lifetime playlistOtherLifetime;
rpl::lifetime sessionLifetime;
rpl::event_stream<> playlistChanges;
History *history = nullptr;
MsgId topicRootId = 0;
PeerId monoforumPeerId = 0;
History *migrated = nullptr;
Main::Session *session = nullptr;
bool isPlaying = false;
bool resumeOnCallEnd = false;
std::unique_ptr<Streamed> streamed;
std::unique_ptr<ShuffleData> shuffleData;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlockerVideo;
};
struct SeekingChanges {
Seeking seeking;
AudioMsgId::Type type;
};
Instance();
~Instance();
friend void start(not_null<Audio::Instance*> instance);
friend void finish(not_null<Audio::Instance*> instance);
void setupShortcuts();
void playStreamed(
const AudioMsgId &audioId,
std::shared_ptr<Streaming::Document> shared);
Streaming::PlaybackOptions streamingOptions(
const AudioMsgId &audioId,
crl::time position = -1);
// Observed notifications.
void handleSongUpdate(const AudioMsgId &audioId);
void pauseOnCall(AudioMsgId::Type type);
void resumeOnCall(AudioMsgId::Type type);
void setCurrent(const AudioMsgId &audioId);
void refreshPlaylist(not_null<Data*> data);
void refreshOtherPlaylist(not_null<Data*> data);
std::optional<SliceKey> playlistKey(not_null<const Data*> data) const;
bool validPlaylist(not_null<const Data*> data) const;
void validatePlaylist(not_null<Data*> data);
std::optional<SliceKey> playlistOtherKey(
not_null<const Data*> data) const;
bool validOtherPlaylist(not_null<const Data*> data) const;
void validateOtherPlaylist(not_null<Data*> data);
void playlistUpdated(not_null<Data*> data);
bool moveInPlaylist(not_null<Data*> data, int delta, bool autonext);
void updatePowerSaveBlocker(
not_null<Data*> data,
const TrackState &state);
HistoryItem *itemByIndex(not_null<Data*> data, int index);
void stopAndClear(not_null<Data*> data);
[[nodiscard]] MsgId computeCurrentUniversalId(
not_null<const Data*> data) const;
void validateShuffleData(not_null<Data*> data);
void setupShuffleData(not_null<Data*> data);
void ensureShuffleMove(not_null<Data*> data, int delta);
void handleStreamingUpdate(
not_null<Data*> data,
Streaming::Update &&update);
void handleStreamingError(
not_null<Data*> data,
Streaming::Error &&error);
void clearStreamed(not_null<Data*> data, bool savePosition = true);
void emitUpdate(AudioMsgId::Type type);
template <typename CheckCallback>
void emitUpdate(AudioMsgId::Type type, CheckCallback check);
[[nodiscard]] RepeatMode repeat(not_null<const Data*> data) const;
[[nodiscard]] rpl::producer<RepeatMode> repeatChanges(
not_null<const Data*> data) const;
[[nodiscard]] OrderMode order(not_null<const Data*> data) const;
[[nodiscard]] rpl::producer<OrderMode> orderChanges(
not_null<const Data*> data) const;
Data *getData(AudioMsgId::Type type) {
if (type == AudioMsgId::Type::Song) {
return &_songData;
} else if (type == AudioMsgId::Type::Voice) {
return &_voiceData;
}
return nullptr;
}
const Data *getData(AudioMsgId::Type type) const {
if (type == AudioMsgId::Type::Song) {
return &_songData;
} else if (type == AudioMsgId::Type::Voice) {
return &_voiceData;
}
return nullptr;
}
HistoryItem *roundVideoItem() const;
void requestRoundVideoResize() const;
void requestRoundVideoRepaint() const;
void setHistory(
not_null<Data*> data,
History *history,
Main::Session *sessionFallback = nullptr);
void setSession(not_null<Data*> data, Main::Session *session);
Data _songData;
Data _voiceData;
bool _roundPlaying = false;
rpl::event_stream<Switch> _switchToNext;
rpl::event_stream<AudioMsgId::Type> _tracksFinished;
rpl::event_stream<AudioMsgId::Type> _trackChanged;
rpl::event_stream<AudioMsgId::Type> _playerStopped;
rpl::event_stream<AudioMsgId::Type> _playerStartedPlay;
rpl::event_stream<TrackState> _updatedNotifier;
rpl::event_stream<SeekingChanges> _seekingChanges;
rpl::event_stream<> _closePlayerRequests;
rpl::lifetime _lifetime;
};
} // namespace Player
} // namespace Media

View File

@@ -0,0 +1,436 @@
/*
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/player/media_player_panel.h"
#include "media/player/media_player_instance.h"
#include "info/media/info_media_list_widget.h"
#include "info/saved/info_saved_music_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/scroll_area.h"
#include "ui/cached_round_corners.h"
#include "ui/ui_utility.h"
#include "mainwindow.h"
#include "main/main_session.h"
#include "styles/style_overview.h"
#include "styles/style_media_player.h"
#include "styles/style_info.h"
namespace Media {
namespace Player {
namespace {
using ListWidget = Info::Media::ListWidget;
constexpr auto kPlaylistIdsLimit = 32;
constexpr auto kDelayedHideTimeout = crl::time(3000);
} // namespace
Panel::Panel(
QWidget *parent,
not_null<Window::SessionController*> window)
: RpWidget(parent)
, AbstractController(window)
, _showTimer([=] { startShow(); })
, _hideTimer([=] { startHideChecked(); })
, _scroll(this, st::mediaPlayerScroll) {
hide();
updateSize();
}
bool Panel::overlaps(const QRect &globalRect) {
if (isHidden() || _a_appearance.animating()) return false;
auto marginLeft = rtl() ? contentRight() : contentLeft();
auto marginRight = rtl() ? contentLeft() : contentRight();
return rect().marginsRemoved(QMargins(marginLeft, contentTop(), marginRight, contentBottom())).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
}
void Panel::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void Panel::listHeightUpdated(int newHeight) {
if (newHeight > emptyInnerHeight()) {
updateSize();
} else {
_hideTimer.callOnce(0);
}
}
bool Panel::contentTooSmall() const {
const auto innerHeight = _scroll->widget()
? _scroll->widget()->height()
: emptyInnerHeight();
return (innerHeight <= emptyInnerHeight());
}
int Panel::emptyInnerHeight() const {
return st::infoMediaMargin.top()
+ st::overviewFileLayout.songPadding.top()
+ st::overviewFileLayout.songThumbSize
+ st::overviewFileLayout.songPadding.bottom()
+ st::infoMediaMargin.bottom();
}
bool Panel::preventAutoHide() const {
if (const auto list = static_cast<ListWidget*>(_scroll->widget())) {
return list->preventAutoHide();
}
return false;
}
void Panel::updateControlsGeometry() {
const auto scrollTop = contentTop();
const auto width = contentWidth();
const auto scrollHeight = qMax(
height() - scrollTop - contentBottom() - scrollMarginBottom(),
0);
if (scrollHeight > 0) {
_scroll->setGeometryToRight(contentRight(), scrollTop, width, scrollHeight);
}
if (const auto widget = static_cast<RpWidget*>(_scroll->widget())) {
widget->resizeToWidth(width);
}
}
int Panel::bestPositionFor(int left) const {
left -= contentLeft();
left -= st::mediaPlayerFileLayout.songPadding.left();
left -= st::mediaPlayerFileLayout.songThumbSize / 2;
return left;
}
void Panel::scrollPlaylistToCurrentTrack() {
if (const auto list = static_cast<ListWidget*>(_scroll->widget())) {
const auto rect = list->getCurrentSongGeometry();
_scroll->scrollToY(rect.y() - st::infoMediaMargin.top());
}
}
void Panel::updateSize() {
auto width = contentLeft() + st::mediaPlayerPanelWidth + contentRight();
auto height = contentTop();
auto listHeight = 0;
if (auto widget = _scroll->widget()) {
listHeight = widget->height();
}
auto scrollVisible = (listHeight > 0);
auto scrollHeight = scrollVisible ? (qMin(listHeight, st::mediaPlayerListHeightMax) + st::mediaPlayerListMarginBottom) : 0;
height += scrollHeight + contentBottom();
resize(width, height);
_scroll->setVisible(scrollVisible);
}
void Panel::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (!_cache.isNull()) {
bool animating = _a_appearance.animating();
if (animating) {
p.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));
} else if (_hiding || isHidden()) {
hideFinished();
return;
}
p.drawPixmap(0, 0, _cache);
if (!animating) {
showChildren();
_cache = QPixmap();
}
return;
}
// draw shadow
auto shadowedRect = myrtlrect(contentLeft(), contentTop(), contentWidth(), contentHeight());
auto shadowedSides = (rtl() ? RectPart::Right : RectPart::Left)
| RectPart::Bottom
| (rtl() ? RectPart::Left : RectPart::Right)
| RectPart::Top;
Ui::Shadow::paint(p, shadowedRect, width(), st::defaultRoundShadow, shadowedSides);
Ui::FillRoundRect(p, shadowedRect, st::menuBg, Ui::MenuCorners);
}
void Panel::enterEventHook(QEnterEvent *e) {
if (_ignoringEnterEvents || contentTooSmall()) return;
_hideTimer.cancel();
if (_a_appearance.animating()) {
startShow();
} else {
_showTimer.callOnce(0);
}
return RpWidget::enterEventHook(e);
}
void Panel::leaveEventHook(QEvent *e) {
if (preventAutoHide()) {
return;
}
_showTimer.cancel();
if (_a_appearance.animating()) {
startHide();
} else {
_hideTimer.callOnce(300);
}
return RpWidget::leaveEventHook(e);
}
void Panel::showFromOther() {
_hideTimer.cancel();
if (_a_appearance.animating()) {
startShow();
} else {
_showTimer.callOnce(300);
}
}
void Panel::hideFromOther() {
_showTimer.cancel();
if (_a_appearance.animating()) {
startHide();
} else {
_hideTimer.callOnce(0);
}
}
void Panel::ensureCreated() {
if (_scroll->widget()) return;
_refreshListLifetime = instance()->playlistChanges(
AudioMsgId::Type::Song
) | rpl::on_next([=] {
refreshList();
});
refreshList();
macWindowDeactivateEvents(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::on_next([=] {
leaveEvent(nullptr);
}, _refreshListLifetime);
_ignoringEnterEvents = false;
}
void Panel::refreshList() {
const auto current = instance()->current(AudioMsgId::Type::Song);
const auto contextId = current.contextId();
auto savedMusicItem = false;
const auto peer = [&]() -> PeerData* {
if (const auto document = current.audio()) {
if (&document->session() != &session()) {
// Different account is playing music.
return nullptr;
}
}
const auto item = contextId
? session().data().message(contextId)
: nullptr;
const auto media = item ? item->media() : nullptr;
const auto document = media ? media->document() : nullptr;
if (!document
|| !document->isSharedMediaMusic()
|| (!item->isRegular()
&& !item->isScheduled()
&& !item->isSavedMusicItem())) {
return nullptr;
}
savedMusicItem = item->isSavedMusicItem();
const auto result = item->history()->peer;
if (const auto migrated = result->migrateTo()) {
return migrated;
}
return result;
}();
const auto migrated = peer ? peer->migrateFrom() : nullptr;
const auto listPeer = savedMusicItem ? nullptr : peer;
const auto listMusicPeer = savedMusicItem ? peer : nullptr;
const auto listMigratedPeer = savedMusicItem ? nullptr : migrated;
if (_listPeer != listPeer
|| _listMusicPeer != listMusicPeer
|| _listMigratedPeer != listMigratedPeer) {
_scroll->takeWidget<QWidget>().destroy();
_listPeer = _listMusicPeer = _listMigratedPeer = nullptr;
}
if ((listPeer && !_listPeer) || (listMusicPeer && !_listMusicPeer)) {
_listPeer = listPeer;
_listMusicPeer = listMusicPeer;
_listMigratedPeer = listMigratedPeer;
auto list = object_ptr<ListWidget>(this, infoController());
const auto weak = _scroll->setOwnedWidget(std::move(list));
updateSize();
updateControlsGeometry();
weak->checkForHide(
) | rpl::on_next([this] {
if (!rect().contains(mapFromGlobal(QCursor::pos()))) {
_hideTimer.callOnce(kDelayedHideTimeout);
}
}, weak->lifetime());
weak->heightValue(
) | rpl::on_next([this](int newHeight) {
listHeightUpdated(newHeight);
}, weak->lifetime());
weak->scrollToRequests(
) | rpl::on_next([this](int newScrollTop) {
_scroll->scrollToY(newScrollTop);
}, weak->lifetime());
// MSVC BUG + REGRESSION rpl::mappers::tuple :(
using namespace rpl::mappers;
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue()
) | rpl::on_next([=](int top, int height) {
const auto bottom = top + height;
weak->setVisibleTopBottom(top, bottom);
}, weak->lifetime());
auto musicMemento = Info::Saved::MusicMemento(peer);
auto mediaMemento = Info::Media::Memento(
peer,
migratedPeerId(),
(listMusicPeer
? Storage::SharedMediaType::MusicFile
: section().mediaType()));
auto &memento = listMusicPeer ? musicMemento.media() : mediaMemento;
memento.setAroundId(contextId);
memento.setIdsLimit(kPlaylistIdsLimit);
memento.setScrollTopItem({ contextId, peer->session().uniqueId() });
memento.setScrollTopShift(-st::infoMediaMargin.top());
weak->restoreState(&memento);
}
}
void Panel::performDestroy() {
if (!_scroll->widget()) return;
_scroll->takeWidget<QWidget>().destroy();
_listPeer = _listMusicPeer = _listMigratedPeer = nullptr;
_refreshListLifetime.destroy();
}
Info::Key Panel::key() const {
return _listMusicPeer
? Info::Key(Info::Saved::MusicTag{ _listMusicPeer })
: Info::Key(_listPeer);
}
PeerData *Panel::migrated() const {
return _listMigratedPeer;
}
Info::Section Panel::section() const {
return _listMusicPeer
? Info::Section(Info::Section::Type::SavedMusic)
: Info::Section(Info::Section::MediaType::MusicFile);
}
void Panel::startShow() {
ensureCreated();
if (contentTooSmall()) {
return;
}
if (isHidden()) {
scrollPlaylistToCurrentTrack();
show();
} else if (!_hiding) {
return;
}
_hiding = false;
startAnimation();
}
void Panel::hideIgnoringEnterEvents() {
_ignoringEnterEvents = true;
if (isHidden()) {
hideFinished();
} else {
startHide();
}
}
void Panel::startHideChecked() {
if (!contentTooSmall() && preventAutoHide()) {
return;
}
if (isHidden()) {
hideFinished();
} else {
startHide();
}
}
void Panel::startHide() {
if (_hiding || isHidden()) return;
_hiding = true;
startAnimation();
}
void Panel::startAnimation() {
auto from = _hiding ? 1. : 0.;
auto to = _hiding ? 0. : 1.;
if (_cache.isNull()) {
showChildren();
_cache = Ui::GrabWidget(this);
}
hideChildren();
_a_appearance.start([this] { appearanceCallback(); }, from, to, st::defaultInnerDropdown.duration);
}
void Panel::appearanceCallback() {
if (!_a_appearance.animating() && _hiding) {
_hiding = false;
hideFinished();
} else {
update();
}
}
void Panel::hideFinished() {
hide();
_cache = QPixmap();
performDestroy();
}
int Panel::contentLeft() const {
return st::mediaPlayerPanelMarginLeft;
}
int Panel::contentTop() const {
return st::mediaPlayerPanelMarginLeft;
}
int Panel::contentRight() const {
return st::mediaPlayerPanelMarginLeft;
}
int Panel::contentBottom() const {
return st::mediaPlayerPanelMarginBottom;
}
int Panel::scrollMarginBottom() const {
return 0;// st::mediaPlayerPanelMarginBottom;
}
} // namespace Player
} // namespace Media

View File

@@ -0,0 +1,110 @@
/*
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
#include "base/timer.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "info/info_controller.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class ScrollArea;
class Shadow;
} // namespace Ui
namespace Media {
namespace Player {
class CoverWidget;
class Panel : public Ui::RpWidget, private Info::AbstractController {
public:
Panel(
QWidget *parent,
not_null<Window::SessionController*> controller);
bool overlaps(const QRect &globalRect);
void hideIgnoringEnterEvents();
void showFromOther();
void hideFromOther();
int bestPositionFor(int left) const;
protected:
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
private:
// Info::AbstractController implementation.
Info::Key key() const override;
PeerData *migrated() const override;
Info::Section section() const override;
void startShow();
void startHide();
void startHideChecked();
bool preventAutoHide() const;
void listHeightUpdated(int newHeight);
int emptyInnerHeight() const;
bool contentTooSmall() const;
void ensureCreated();
void performDestroy();
void updateControlsGeometry();
void refreshList();
void updateSize();
void appearanceCallback();
void hideFinished();
int contentLeft() const;
int contentTop() const;
int contentRight() const;
int contentBottom() const;
int scrollMarginBottom() const;
int contentWidth() const {
return width() - contentLeft() - contentRight();
}
int contentHeight() const {
return height() - contentTop() - contentBottom();
}
void startAnimation();
void scrollPlaylistToCurrentTrack();
not_null<Info::AbstractController*> infoController() {
return static_cast<Info::AbstractController*>(this);
}
bool _hiding = false;
QPixmap _cache;
Ui::Animations::Simple _a_appearance;
bool _ignoringEnterEvents = false;
base::Timer _showTimer;
base::Timer _hideTimer;
object_ptr<Ui::ScrollArea> _scroll;
rpl::lifetime _refreshListLifetime;
PeerData *_listPeer = nullptr;
PeerData *_listMusicPeer = nullptr;
PeerData *_listMigratedPeer = nullptr;
};
} // namespace Player
} // namespace Media

View File

@@ -0,0 +1,116 @@
/*
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/player/media_player_volume_controller.h"
#include "media/audio/media_audio.h"
#include "media/player/media_player_dropdown.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "mainwindow.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "styles/style_media_player.h"
#include "styles/style_widgets.h"
#include <QtGui/QGuiApplication>
namespace Media::Player {
VolumeController::VolumeController(
QWidget *parent,
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _slider(this, st::mediaPlayerPanelPlayback) {
_slider->setMoveByWheel(true);
_slider->setChangeProgressCallback([=](float64 volume) {
applyVolumeChange(volume);
});
_slider->setChangeFinishedCallback([=](float64 volume) {
if (volume > 0) {
Core::App().settings().setRememberedSongVolume(volume);
}
applyVolumeChange(volume);
Core::App().saveSettingsDelayed();
});
Core::App().settings().songVolumeChanges(
) | rpl::on_next([=](float64 volume) {
if (!_slider->isChanging()) {
_slider->setValue(volume);
}
}, lifetime());
setVolume(Core::App().settings().songVolume());
resize(st::mediaPlayerPanelVolumeWidth, 2 * st::mediaPlayerPanelPlaybackPadding + st::mediaPlayerPanelPlayback.width);
}
void VolumeController::setIsVertical(bool vertical) {
using Direction = Ui::MediaSlider::Direction;
_slider->setDirection(vertical ? Direction::Vertical : Direction::Horizontal);
_slider->setAlwaysDisplayMarker(vertical);
}
void VolumeController::outerWheelEvent(not_null<QWheelEvent*> e) {
QGuiApplication::sendEvent(_slider.data(), e);
}
void VolumeController::resizeEvent(QResizeEvent *e) {
_slider->setGeometry(rect());
}
void VolumeController::setVolume(float64 volume) {
_slider->setValue(volume);
if (volume > 0) {
Core::App().settings().setRememberedSongVolume(volume);
}
applyVolumeChange(volume);
}
void VolumeController::applyVolumeChange(float64 volume) {
if (volume != Core::App().settings().songVolume()) {
mixer()->setSongVolume(volume);
Core::App().settings().setSongVolume(volume);
}
}
void PrepareVolumeDropdown(
not_null<Dropdown*> dropdown,
not_null<Window::SessionController*> controller,
rpl::producer<not_null<QWheelEvent*>> outerWheelEvents) {
const auto volume = Ui::CreateChild<VolumeController>(
dropdown.get(),
controller);
volume->show();
volume->setIsVertical(true);
dropdown->sizeValue(
) | rpl::on_next([=](QSize size) {
const auto rect = QRect(QPoint(), size);
const auto inner = rect.marginsRemoved(dropdown->getMargin());
volume->setGeometry(
inner.x(),
inner.y() - st::lineWidth,
inner.width(),
(inner.height()
+ st::lineWidth
- ((st::mediaPlayerVolumeSize.width()
- st::mediaPlayerPanelPlayback.width) / 2)));
}, volume->lifetime());
std::move(
outerWheelEvents
) | rpl::on_next([=](not_null<QWheelEvent*> e) {
volume->outerWheelEvent(e);
}, volume->lifetime());
}
} // namespace Media::Player

View File

@@ -0,0 +1,52 @@
/*
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
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
class QWheelEvent;
namespace Ui {
class MediaSlider;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Media::Player {
class Dropdown;
class VolumeController final : public Ui::RpWidget {
public:
VolumeController(
QWidget *parent,
not_null<Window::SessionController*> controller);
void setIsVertical(bool vertical);
void outerWheelEvent(not_null<QWheelEvent*> e);
protected:
void resizeEvent(QResizeEvent *e) override;
private:
void setVolume(float64 volume);
void applyVolumeChange(float64 volume);
object_ptr<Ui::MediaSlider> _slider;
};
void PrepareVolumeDropdown(
not_null<Dropdown*> dropdown,
not_null<Window::SessionController*> controller,
rpl::producer<not_null<QWheelEvent*>> outerWheelEvents);
} // namespace Media::Player

View File

@@ -0,0 +1,748 @@
/*
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/player/media_player_widget.h"
#include "platform/platform_specific.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/format_song_document_name.h"
#include "lang/lang_keys.h"
#include "media/audio/media_audio.h"
#include "media/view/media_view_playback_progress.h"
#include "media/player/media_player_button.h"
#include "media/player/media_player_instance.h"
#include "media/player/media_player_dropdown.h"
#include "media/player/media_player_volume_controller.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "storage/storage_account.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "styles/style_media_player.h"
#include "styles/style_media_view.h"
#include "styles/style_chat.h" // expandedMenuSeparator.
namespace Media {
namespace Player {
Widget::Widget(
QWidget *parent,
not_null<Ui::RpWidget*> dropdownsParent,
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _controller(controller)
, _orderMenuParent(dropdownsParent)
, _nameLabel(this, st::mediaPlayerName)
, _rightControls(this, object_ptr<Ui::RpWidget>(this))
, _timeLabel(rightControls(), st::mediaPlayerTime)
, _playPause(this, st::mediaPlayerPlayButton)
, _volumeToggle(rightControls(), st::mediaPlayerVolumeToggle)
, _repeatToggle(rightControls(), st::mediaPlayerRepeatButton)
, _orderToggle(rightControls(), st::mediaPlayerOrderButton)
, _speedToggle(rightControls(), st::mediaPlayerSpeedButton)
, _close(this, st::mediaPlayerClose)
, _shadow(this)
, _playbackSlider(this, st::mediaPlayerPlayback)
, _volume(std::in_place, dropdownsParent.get())
, _playbackProgress(std::make_unique<View::PlaybackProgress>())
, _orderController(
std::make_unique<OrderController>(
_orderToggle.data(),
dropdownsParent,
[=](bool over) { markOver(over); },
Core::App().settings().playerOrderModeValue(),
[=](OrderMode value) { saveOrder(value); }))
, _speedController(
std::make_unique<SpeedController>(
_speedToggle.data(),
_speedToggle->st(),
dropdownsParent,
[=](bool over) { markOver(over); },
[=](bool lastNonDefault) { return speedLookup(lastNonDefault); },
[=](float64 speed) { saveSpeed(speed); })) {
_speedController->realtimeValue(
) | rpl::on_next([=](float64 speed) {
_speedToggle->setSpeed(speed);
}, _speedToggle->lifetime());
_speedToggle->finishAnimating();
setAttribute(Qt::WA_OpaquePaintEvent);
setMouseTracking(true);
resize(width(), st::mediaPlayerHeight + st::lineWidth);
setupRightControls();
_nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
_timeLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
_playbackProgress->setInLoadingStateChangedCallback([=](bool loading) {
_playbackSlider->setDisabled(loading);
});
_playbackProgress->setValueChangedCallback([=](float64 value, float64) {
_playbackSlider->setValue(value);
});
_playbackSlider->setChangeProgressCallback([=](float64 value) {
if (_type != AudioMsgId::Type::Song) {
return; // Round video seek is not supported for now :(
}
_playbackProgress->setValue(value, false);
handleSeekProgress(value);
});
_playbackSlider->setChangeFinishedCallback([=](float64 value) {
if (_type != AudioMsgId::Type::Song) {
return; // Round video seek is not supported for now :(
}
_playbackProgress->setValue(value, false);
handleSeekFinished(value);
});
_playPause->setClickedCallback([=] {
instance()->playPauseCancelClicked(_type);
});
updateVolumeToggleIcon();
_volumeToggle->setClickedCallback([=] {
const auto volume = (Core::App().settings().songVolume() > 0)
? 0.
: Core::App().settings().rememberedSongVolume();
Core::App().settings().setSongVolume(volume);
Core::App().saveSettingsDelayed();
mixer()->setSongVolume(volume);
});
Core::App().settings().songVolumeChanges(
) | rpl::on_next([=] {
updateVolumeToggleIcon();
}, lifetime());
Core::App().settings().playerRepeatModeValue(
) | rpl::on_next([=] {
updateRepeatToggleIcon();
}, lifetime());
_repeatToggle->setClickedCallback([=] {
auto &settings = Core::App().settings();
settings.setPlayerRepeatMode([&] {
switch (settings.playerRepeatMode()) {
case RepeatMode::None: return RepeatMode::One;
case RepeatMode::One: return RepeatMode::All;
case RepeatMode::All: return RepeatMode::None;
}
Unexpected("Repeat mode in Settings.");
}());
Core::App().saveSettingsDelayed();
});
_speedController->saved(
) | rpl::on_next([=] {
instance()->updateVoicePlaybackSpeed();
}, lifetime());
instance()->trackChanged(
) | rpl::filter([=](AudioMsgId::Type type) {
return (type == _type);
}) | rpl::on_next([=](AudioMsgId::Type type) {
handleSongChange();
updateControlsVisibility();
updateLabelsGeometry();
}, lifetime());
instance()->tracksFinished(
) | rpl::filter([=](AudioMsgId::Type type) {
return (type == AudioMsgId::Type::Voice);
}) | rpl::on_next([=](AudioMsgId::Type type) {
_voiceIsActive = false;
const auto currentSong = instance()->current(AudioMsgId::Type::Song);
const auto songState = instance()->getState(AudioMsgId::Type::Song);
if (currentSong == songState.id && !IsStoppedOrStopping(songState.state)) {
setType(AudioMsgId::Type::Song);
}
}, lifetime());
instance()->updatedNotifier(
) | rpl::on_next([=](const TrackState &state) {
handleSongUpdate(state);
}, lifetime());
PrepareVolumeDropdown(_volume.get(), controller, _volumeToggle->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Wheel);
}) | rpl::map([=](not_null<QEvent*> e) {
return not_null{ static_cast<QWheelEvent*>(e.get()) };
}));
_volumeToggle->installEventFilter(_volume.get());
_volume->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::Enter) {
markOver(true);
} else if (e->type() == QEvent::Leave) {
markOver(false);
}
}, _volume->lifetime());
hidePlaylistOn(_playPause);
hidePlaylistOn(_close);
hidePlaylistOn(_rightControls);
setType(AudioMsgId::Type::Song);
}
void Widget::hidePlaylistOn(not_null<Ui::RpWidget*> widget) {
widget->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::on_next([=] {
updateOverLabelsState(false);
}, widget->lifetime());
}
void Widget::setupRightControls() {
const auto raw = rightControls();
raw->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(raw);
const auto &icon = st::mediaPlayerControlsFade;
const auto fade = QRect(0, 0, icon.width(), raw->height());
if (fade.intersects(clip)) {
icon.fill(p, fade);
}
const auto fill = clip.intersected(
{ icon.width(), 0, raw->width() - icon.width(), raw->height() });
if (!fill.isEmpty()) {
p.fillRect(fill, st::mediaPlayerBg);
}
}, raw->lifetime());
_rightControls->show(anim::type::instant);
}
void Widget::updateVolumeToggleIcon() {
_volumeToggle->setIconOverride([] {
const auto volume = Core::App().settings().songVolume();
return (volume == 0.)
? &st::mediaPlayerVolumeIcon0
: (volume < 0.66)
? &st::mediaPlayerVolumeIcon1
: nullptr;
}());
}
void Widget::setCloseCallback(Fn<void()> callback) {
_closeCallback = std::move(callback);
_close->setClickedCallback([this] { stopAndClose(); });
}
void Widget::setShowItemCallback(
Fn<void(not_null<const HistoryItem*>)> callback) {
_showItemCallback = std::move(callback);
}
void Widget::stopAndClose() {
_voiceIsActive = false;
if (_type == AudioMsgId::Type::Voice) {
const auto songData = instance()->current(AudioMsgId::Type::Song);
const auto songState = instance()->getState(AudioMsgId::Type::Song);
if (songData == songState.id && !IsStoppedOrStopping(songState.state)) {
instance()->stop(AudioMsgId::Type::Voice);
return;
}
}
if (_closeCallback) {
_closeCallback();
}
}
void Widget::setShadowGeometryToLeft(int x, int y, int w, int h) {
_shadow->setGeometryToLeft(x, y, w, h);
}
void Widget::showShadowAndDropdowns() {
_shadow->show();
_playbackSlider->setVisible(_type == AudioMsgId::Type::Song);
if (_volumeHidden) {
_volumeHidden = false;
_volume->show();
}
_speedController->showBack();
_orderController->showBack();
}
void Widget::updateDropdownsGeometry() {
const auto dropdownWidth = st::mediaPlayerVolumeSize.width();
const auto position = _volume->parentWidget()->mapFromGlobal(
_volumeToggle->mapToGlobal(
QPoint(
(_volumeToggle->width() - dropdownWidth) / 2,
height())));
const auto playerMargins = _volume->getMargin();
const auto shift = QPoint(playerMargins.left(), playerMargins.top());
_volume->move(position - shift);
_orderController->updateDropdownGeometry();
_speedController->updateDropdownGeometry();
}
void Widget::hideShadowAndDropdowns() {
_shadow->hide();
_playbackSlider->hide();
if (!_volume->isHidden()) {
_volumeHidden = true;
_volume->hide();
}
_speedController->hideTemporarily();
_orderController->hideTemporarily();
}
void Widget::raiseDropdowns() {
_volume->raise();
}
Widget::~Widget() = default;
not_null<Ui::RpWidget*> Widget::rightControls() {
return _rightControls->entity();
}
void Widget::handleSeekProgress(float64 progress) {
if (!_lastDurationMs) return;
const auto positionMs = std::clamp(
static_cast<crl::time>(progress * _lastDurationMs),
crl::time(0),
_lastDurationMs);
if (_seekPositionMs != positionMs) {
_seekPositionMs = positionMs;
updateTimeLabel();
instance()->startSeeking(_type);
}
}
void Widget::handleSeekFinished(float64 progress) {
if (!_lastDurationMs) return;
_seekPositionMs = -1;
instance()->finishSeeking(_type, progress);
}
void Widget::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
_narrow = (width() < st::mediaPlayerWideWidth);
updateControlsWrapVisibility();
}
void Widget::updateControlsGeometry() {
_close->moveToRight(st::mediaPlayerCloseRight, st::mediaPlayerPlayTop);
auto right = 0;
if (hasPlaybackSpeedControl()) {
_speedToggle->moveToRight(right, 0); right += _speedToggle->width();
}
if (_type == AudioMsgId::Type::Song) {
_repeatToggle->moveToRight(right, 0); right += _repeatToggle->width();
_orderToggle->moveToRight(right, 0); right += _orderToggle->width();
}
_volumeToggle->moveToRight(right, 0); right += _volumeToggle->width();
updateControlsWrapGeometry();
updatePlayPrevNextPositions();
_playbackSlider->setGeometry(
0,
height() - st::mediaPlayerPlayback.fullWidth,
width(),
st::mediaPlayerPlayback.fullWidth);
updateDropdownsGeometry();
}
void Widget::updateControlsWrapGeometry() {
const auto fade = st::mediaPlayerControlsFade.width();
const auto controls = getTimeRight() + _timeLabel->width() + fade;
rightControls()->resize(controls, _repeatToggle->height());
_rightControls->move(
width() - st::mediaPlayerCloseRight - _close->width() - controls,
st::mediaPlayerPlayTop);
}
void Widget::updateControlsWrapVisibility() {
_rightControls->toggle(
_over || !_narrow,
isHidden() ? anim::type::instant : anim::type::normal);
}
void Widget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto fill = e->rect().intersected(QRect(0, 0, width(), st::mediaPlayerHeight));
if (!fill.isEmpty()) {
p.fillRect(fill, st::mediaPlayerBg);
}
}
void Widget::enterEventHook(QEnterEvent *e) {
markOver(true);
}
void Widget::leaveEventHook(QEvent *e) {
markOver(false);
}
void Widget::markOver(bool over) {
if (over) {
_over = true;
_wontBeOver = false;
InvokeQueued(this, [=] {
updateControlsWrapVisibility();
});
} else {
_wontBeOver = true;
InvokeQueued(this, [=] {
if (!_wontBeOver) {
return;
}
_wontBeOver = false;
_over = false;
updateControlsWrapVisibility();
});
updateOverLabelsState(false);
}
}
void Widget::saveOrder(OrderMode mode) {
Core::App().settings().setPlayerOrderMode(mode);
Core::App().saveSettingsDelayed();
}
float64 Widget::speedLookup(bool lastNonDefault) const {
return Core::App().settings().voicePlaybackSpeed(lastNonDefault);
}
void Widget::saveSpeed(float64 speed) {
Core::App().settings().setVoicePlaybackSpeed(speed);
Core::App().saveSettingsDelayed();
}
void Widget::mouseMoveEvent(QMouseEvent *e) {
updateOverLabelsState(e->pos());
}
void Widget::mousePressEvent(QMouseEvent *e) {
_labelsDown = _labelsOver;
}
void Widget::mouseReleaseEvent(QMouseEvent *e) {
if (auto downLabels = base::take(_labelsDown)) {
if (_labelsOver != downLabels) {
return;
}
if ((_type == AudioMsgId::Type::Voice)
|| _lastSongFromAnotherSession) {
const auto current = instance()->current(_type);
const auto document = current.audio();
const auto context = current.contextId();
if (document && context && _showItemCallback) {
if (const auto item = document->owner().message(context)) {
_showItemCallback(item);
}
}
}
}
}
void Widget::updateOverLabelsState(QPoint pos) {
const auto left = getNameLeft();
const auto right = width()
- _rightControls->x()
- _rightControls->width()
+ getTimeRight();
const auto labels = myrtlrect(left, 0, width() - right - left, height() - st::mediaPlayerPlayback.fullWidth);
const auto over = labels.contains(pos);
updateOverLabelsState(over);
}
void Widget::updateOverLabelsState(bool over) {
_labelsOver = over;
const auto pressShowsItem = _labelsOver
&& ((_type == AudioMsgId::Type::Voice)
|| _lastSongFromAnotherSession);
setCursor(pressShowsItem ? style::cur_pointer : style::cur_default);
_togglePlaylistRequests.fire(over && (_type == AudioMsgId::Type::Song));
}
void Widget::updatePlayPrevNextPositions() {
auto left = st::mediaPlayerPlayLeft;
auto top = st::mediaPlayerPlayTop;
if (_previousTrack) {
_previousTrack->moveToLeft(left, top); left += _previousTrack->width() + st::mediaPlayerPlaySkip;
_playPause->moveToLeft(left, top); left += _playPause->width() + st::mediaPlayerPlaySkip;
_nextTrack->moveToLeft(left, top);
} else {
_playPause->moveToLeft(left, top);
}
updateLabelsGeometry();
}
int Widget::getNameLeft() const {
auto result = st::mediaPlayerPlayLeft + _playPause->width();
if (_previousTrack) {
result += _previousTrack->width() + st::mediaPlayerPlaySkip + _nextTrack->width() + st::mediaPlayerPlaySkip;
}
result += st::mediaPlayerPadding;
return result;
}
int Widget::getNameRight() const {
return st::mediaPlayerCloseRight
+ _close->width()
+ st::mediaPlayerPadding;
}
int Widget::getTimeRight() const {
auto result = 0;
result += _volumeToggle->width();
if (_type == AudioMsgId::Type::Song) {
result += _repeatToggle->width()
+ _orderToggle->width();
}
if (hasPlaybackSpeedControl()) {
result += _speedToggle->width();
}
result += st::mediaPlayerPadding;
return result;
}
void Widget::updateLabelsGeometry() {
const auto left = getNameLeft();
const auto widthForName = width()
- left
- getNameRight();
_nameLabel->resizeToNaturalWidth(widthForName);
_nameLabel->moveToLeft(left, st::mediaPlayerNameTop - st::mediaPlayerName.style.font->ascent);
const auto right = getTimeRight();
_timeLabel->moveToRight(right, st::mediaPlayerNameTop - st::mediaPlayerTime.font->ascent);
updateControlsWrapGeometry();
}
void Widget::updateRepeatToggleIcon() {
switch (Core::App().settings().playerRepeatMode()) {
case RepeatMode::None:
_repeatToggle->setIconOverride(
&st::mediaPlayerRepeatDisabledIcon,
&st::mediaPlayerRepeatDisabledIconOver);
_repeatToggle->setRippleColorOverride(
&st::mediaPlayerRepeatDisabledRippleBg);
break;
case RepeatMode::One:
_repeatToggle->setIconOverride(&st::mediaPlayerRepeatOneIcon);
_repeatToggle->setRippleColorOverride(nullptr);
break;
case RepeatMode::All:
_repeatToggle->setIconOverride(nullptr);
_repeatToggle->setRippleColorOverride(nullptr);
break;
}
}
void Widget::checkForTypeChange() {
auto hasActiveType = [](AudioMsgId::Type type) {
const auto current = instance()->current(type);
const auto state = instance()->getState(type);
return (current == state.id && !IsStoppedOrStopping(state.state));
};
if (hasActiveType(AudioMsgId::Type::Voice)) {
_voiceIsActive = true;
setType(AudioMsgId::Type::Voice);
} else if (!_voiceIsActive && hasActiveType(AudioMsgId::Type::Song)) {
setType(AudioMsgId::Type::Song);
}
}
bool Widget::hasPlaybackSpeedControl() const {
return _lastSongId.changeablePlaybackSpeed()
&& Media::Audio::SupportsSpeedControl();
}
void Widget::updateControlsVisibility() {
_repeatToggle->setVisible(_type == AudioMsgId::Type::Song);
_orderToggle->setVisible(_type == AudioMsgId::Type::Song);
_speedToggle->setVisible(hasPlaybackSpeedControl());
if (!_shadow->isHidden()) {
_playbackSlider->setVisible(_type == AudioMsgId::Type::Song);
}
updateControlsGeometry();
}
void Widget::setType(AudioMsgId::Type type) {
if (_type != type) {
_type = type;
handleSongChange();
updateControlsVisibility();
updateLabelsGeometry();
handleSongUpdate(instance()->getState(_type));
updateOverLabelsState(_labelsOver);
_playlistChangesLifetime = instance()->playlistChanges(
_type
) | rpl::on_next([=] {
handlePlaylistUpdate();
});
// maybe the type change causes a change of the button layout
QResizeEvent event = { size(), size() };
resizeEvent(&event);
}
}
void Widget::handleSongUpdate(const TrackState &state) {
checkForTypeChange();
if (state.id.type() != _type || !state.id.audio()) {
return;
}
if (state.id.audio()->loading()) {
_playbackProgress->updateLoadingState(state.id.audio()->progress());
} else {
_playbackProgress->updateState(state);
}
auto showPause = ShowPauseIcon(state.state);
if (instance()->isSeeking(_type)) {
showPause = true;
}
_playPause->setIconOverride(state.id.audio()->loading()
? &st::mediaPlayerCancelIcon
: showPause
? &st::mediaPlayerPauseIcon
: nullptr);
updateTimeText(state);
}
void Widget::updateTimeText(const TrackState &state) {
qint64 display = 0;
const auto frequency = state.frequency;
const auto document = state.id.audio();
if (!IsStoppedOrStopping(state.state)) {
display = state.position;
} else if (state.length) {
display = state.length;
} else if (document->song()) {
display = (document->duration() * frequency) / 1000;
}
_lastDurationMs = (state.length * 1000LL) / frequency;
if (document->loading()) {
_time = QString::number(qRound(document->progress() * 100)) + '%';
_playbackSlider->setDisabled(true);
} else {
display = display / frequency;
_time = Ui::FormatDurationText(display);
_playbackSlider->setDisabled(false);
}
if (_seekPositionMs < 0) {
updateTimeLabel();
}
}
void Widget::updateTimeLabel() {
auto timeLabelWidth = _timeLabel->width();
if (_seekPositionMs >= 0) {
auto playAlready = _seekPositionMs / 1000LL;
_timeLabel->setText(Ui::FormatDurationText(playAlready));
} else {
_timeLabel->setText(_time);
}
if (timeLabelWidth != _timeLabel->width()) {
updateLabelsGeometry();
}
}
void Widget::handleSongChange() {
const auto current = instance()->current(_type);
const auto document = current.audio();
_lastSongFromAnotherSession = document
&& (document->session().uniqueId()
!= _controller->session().uniqueId());
if (!current
|| !document
|| ((_lastSongId.audio() == document)
&& (_lastSongId.contextId() == current.contextId()))) {
return;
}
_lastSongId = current;
auto textWithEntities = TextWithEntities();
if (document->isVoiceMessage() || document->isVideoMessage()) {
textWithEntities = Ui::Text::FormatVoiceName(
document,
current.contextId()).textWithEntities(true);
} else {
textWithEntities = Ui::Text::FormatSongNameFor(document)
.textWithEntities(true);
}
_nameLabel->setMarkedText(textWithEntities);
handlePlaylistUpdate();
updateLabelsGeometry();
}
void Widget::handlePlaylistUpdate() {
const auto previousEnabled = instance()->previousAvailable(_type);
const auto nextEnabled = instance()->nextAvailable(_type);
if (!previousEnabled && !nextEnabled) {
destroyPrevNextButtons();
} else {
createPrevNextButtons();
_previousTrack->setIconOverride(previousEnabled ? nullptr : &st::mediaPlayerPreviousDisabledIcon);
_previousTrack->setRippleColorOverride(previousEnabled ? nullptr : &st::mediaPlayerBg);
_previousTrack->setPointerCursor(previousEnabled);
_nextTrack->setIconOverride(nextEnabled ? nullptr : &st::mediaPlayerNextDisabledIcon);
_nextTrack->setRippleColorOverride(nextEnabled ? nullptr : &st::mediaPlayerBg);
_nextTrack->setPointerCursor(nextEnabled);
}
}
void Widget::createPrevNextButtons() {
if (!_previousTrack) {
_previousTrack.create(this, st::mediaPlayerPreviousButton);
_previousTrack->show();
_previousTrack->setClickedCallback([=]() {
instance()->previous(_type);
});
_nextTrack.create(this, st::mediaPlayerNextButton);
_nextTrack->show();
_nextTrack->setClickedCallback([=]() {
instance()->next(_type);
});
hidePlaylistOn(_previousTrack);
hidePlaylistOn(_nextTrack);
updatePlayPrevNextPositions();
}
}
void Widget::destroyPrevNextButtons() {
if (_previousTrack) {
_previousTrack.destroy();
_nextTrack.destroy();
updatePlayPrevNextPositions();
}
}
} // namespace Player
} // namespace Media

View File

@@ -0,0 +1,165 @@
/*
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
#include "data/data_audio_msg_id.h"
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
class AudioMsgId;
namespace Ui {
class FlatLabel;
class LabelSimple;
class IconButton;
class PlainShadow;
class FilledSlider;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Media {
enum class OrderMode;
} // namespace Media
namespace Media::View {
class PlaybackProgress;
} // namespace Media::View
namespace Window {
class SessionController;
} // namespace Window
namespace Media::Player {
class Dropdown;
class SpeedButton;
class OrderController;
class SpeedController;
struct TrackState;
class Widget final : public Ui::RpWidget {
public:
Widget(
QWidget *parent,
not_null<Ui::RpWidget*> dropdownsParent,
not_null<Window::SessionController*> controller);
~Widget();
void setCloseCallback(Fn<void()> callback);
void setShowItemCallback(Fn<void(not_null<const HistoryItem*>)> callback);
void stopAndClose();
void setShadowGeometryToLeft(int x, int y, int w, int h);
void hideShadowAndDropdowns();
void showShadowAndDropdowns();
void updateDropdownsGeometry();
void raiseDropdowns();
[[nodiscard]] rpl::producer<bool> togglePlaylistRequests() const {
return _togglePlaylistRequests.events();
}
private:
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
[[nodiscard]] not_null<Ui::RpWidget*> rightControls();
void setupRightControls();
void handleSeekProgress(float64 progress);
void handleSeekFinished(float64 progress);
[[nodiscard]] int getNameLeft() const;
[[nodiscard]] int getNameRight() const;
[[nodiscard]] int getTimeRight() const;
void updateOverLabelsState(QPoint pos);
void updateOverLabelsState(bool over);
void hidePlaylistOn(not_null<Ui::RpWidget*> widget);
void updatePlayPrevNextPositions();
void updateLabelsGeometry();
void updateRepeatToggleIcon();
void updateControlsVisibility();
void updateControlsGeometry();
void updateControlsWrapGeometry();
void updateControlsWrapVisibility();
void createPrevNextButtons();
void destroyPrevNextButtons();
bool hasPlaybackSpeedControl() const;
void updateVolumeToggleIcon();
void checkForTypeChange();
void setType(AudioMsgId::Type type);
void handleSongUpdate(const TrackState &state);
void handleSongChange();
void handlePlaylistUpdate();
void updateTimeText(const TrackState &state);
void updateTimeLabel();
void markOver(bool over);
void saveOrder(OrderMode mode);
[[nodiscard]] float64 speedLookup(bool lastNonDefault) const;
void saveSpeed(float64 speed);
const not_null<Window::SessionController*> _controller;
const not_null<Ui::RpWidget*> _orderMenuParent;
crl::time _seekPositionMs = -1;
crl::time _lastDurationMs = 0;
QString _time;
// We display all the controls according to _type.
// We switch to Type::Voice if a voice/video message is played.
// We switch to Type::Song only if _voiceIsActive == false.
// We change _voiceIsActive to false only manually or from tracksFinished().
AudioMsgId::Type _type = AudioMsgId::Type::Unknown;
AudioMsgId _lastSongId;
bool _lastSongFromAnotherSession = false;
bool _voiceIsActive = false;
Fn<void()> _closeCallback;
Fn<void(not_null<const HistoryItem*>)> _showItemCallback;
bool _labelsOver = false;
bool _labelsDown = false;
rpl::event_stream<bool> _togglePlaylistRequests;
bool _narrow = false;
bool _over = false;
bool _wontBeOver = false;
bool _volumeHidden = false;
object_ptr<Ui::FlatLabel> _nameLabel;
object_ptr<Ui::FadeWrap<Ui::RpWidget>> _rightControls;
object_ptr<Ui::LabelSimple> _timeLabel;
object_ptr<Ui::IconButton> _previousTrack = { nullptr };
object_ptr<Ui::IconButton> _playPause;
object_ptr<Ui::IconButton> _nextTrack = { nullptr };
object_ptr<Ui::IconButton> _volumeToggle;
object_ptr<Ui::IconButton> _repeatToggle;
object_ptr<Ui::IconButton> _orderToggle;
object_ptr<SpeedButton> _speedToggle;
object_ptr<Ui::IconButton> _close;
object_ptr<Ui::PlainShadow> _shadow = { nullptr };
object_ptr<Ui::FilledSlider> _playbackSlider;
base::unique_qptr<Dropdown> _volume;
std::unique_ptr<View::PlaybackProgress> _playbackProgress;
std::unique_ptr<OrderController> _orderController;
std::unique_ptr<SpeedController> _speedController;
rpl::lifetime _playlistChangesLifetime;
};
} // namespace Media::Player