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
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:
338
Telegram/SourceFiles/media/player/media_player.style
Normal file
338
Telegram/SourceFiles/media/player/media_player.style
Normal 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 }};
|
||||
505
Telegram/SourceFiles/media/player/media_player_button.cpp
Normal file
505
Telegram/SourceFiles/media/player/media_player_button.cpp
Normal 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
|
||||
143
Telegram/SourceFiles/media/player/media_player_button.h
Normal file
143
Telegram/SourceFiles/media/player/media_player_button.h
Normal 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
|
||||
865
Telegram/SourceFiles/media/player/media_player_dropdown.cpp
Normal file
865
Telegram/SourceFiles/media/player/media_player_dropdown.cpp
Normal 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
|
||||
167
Telegram/SourceFiles/media/player/media_player_dropdown.h
Normal file
167
Telegram/SourceFiles/media/player/media_player_dropdown.h
Normal 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
|
||||
667
Telegram/SourceFiles/media/player/media_player_float.cpp
Normal file
667
Telegram/SourceFiles/media/player/media_player_float.cpp
Normal 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
|
||||
287
Telegram/SourceFiles/media/player/media_player_float.h
Normal file
287
Telegram/SourceFiles/media/player/media_player_float.h
Normal 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
|
||||
1405
Telegram/SourceFiles/media/player/media_player_instance.cpp
Normal file
1405
Telegram/SourceFiles/media/player/media_player_instance.cpp
Normal file
File diff suppressed because it is too large
Load Diff
321
Telegram/SourceFiles/media/player/media_player_instance.h
Normal file
321
Telegram/SourceFiles/media/player/media_player_instance.h
Normal 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
|
||||
436
Telegram/SourceFiles/media/player/media_player_panel.cpp
Normal file
436
Telegram/SourceFiles/media/player/media_player_panel.cpp
Normal 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
|
||||
110
Telegram/SourceFiles/media/player/media_player_panel.h
Normal file
110
Telegram/SourceFiles/media/player/media_player_panel.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
748
Telegram/SourceFiles/media/player/media_player_widget.cpp
Normal file
748
Telegram/SourceFiles/media/player/media_player_widget.cpp
Normal 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
|
||||
165
Telegram/SourceFiles/media/player/media_player_widget.h
Normal file
165
Telegram/SourceFiles/media/player/media_player_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user