766 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			766 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
This file is part of Telegram Desktop,
 | 
						|
the official desktop application for the Telegram messaging service.
 | 
						|
 | 
						|
For license and copyright information please follow this link:
 | 
						|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | 
						|
*/
 | 
						|
#include "media/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 "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::start_with_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::start_with_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::start_with_next([=](float64 external) {
 | 
						|
		setExternalValue(external);
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	_last.value(
 | 
						|
	) | rpl::start_with_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) {
 | 
						|
	auto slider = base::make_unique_q<SpeedSliderItem>(
 | 
						|
		menu,
 | 
						|
		st,
 | 
						|
		rpl::duplicate(value));
 | 
						|
 | 
						|
	slider->debouncedChanges(
 | 
						|
	) | rpl::start_with_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));
 | 
						|
	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::start_with_next([check, icon = &st.activeCheck] {
 | 
						|
			auto p = QPainter(check);
 | 
						|
			icon->paint(p, 0, 0, check->width());
 | 
						|
		}, check->lifetime());
 | 
						|
		raw->sizeValue(
 | 
						|
		) | rpl::start_with_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::start_with_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::start_with_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::start_with_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);
 | 
						|
}
 | 
						|
 | 
						|
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::start_with_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([=]{
 | 
						|
		Ui::PostponeCall(raw, [this] {
 | 
						|
			_menu = nullptr;
 | 
						|
		});
 | 
						|
	});
 | 
						|
	_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);
 | 
						|
}
 | 
						|
 | 
						|
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::start_with_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<SpeedButton*> button,
 | 
						|
	not_null<QWidget*> menuParent,
 | 
						|
	Fn<void(bool)> menuOverCallback,
 | 
						|
	Fn<float64(bool lastNonDefault)> value,
 | 
						|
	Fn<void(float64)> change)
 | 
						|
: WithDropdownController(
 | 
						|
	button,
 | 
						|
	menuParent,
 | 
						|
	button->st().menu.dropdown,
 | 
						|
	button->st().menuAlign,
 | 
						|
	std::move(menuOverCallback))
 | 
						|
, _st(button->st())
 | 
						|
, _lookup(std::move(value))
 | 
						|
, _change(std::move(change)) {
 | 
						|
	button->setClickedCallback([=] {
 | 
						|
		toggleDefault();
 | 
						|
		save();
 | 
						|
		if (const auto current = menu()) {
 | 
						|
			current->otherEnter();
 | 
						|
		}
 | 
						|
	});
 | 
						|
 | 
						|
	setSpeed(_lookup(false));
 | 
						|
	_speed = _lookup(true);
 | 
						|
 | 
						|
	button->setSpeed(_speed, anim::type::instant);
 | 
						|
 | 
						|
	_speedChanged.events_starting_with(
 | 
						|
		speed()
 | 
						|
	) | rpl::start_with_next([=](float64 speed) {
 | 
						|
		button->setSpeed(speed);
 | 
						|
	}, button->lifetime());
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<> SpeedController::saved() const {
 | 
						|
	return _saved.events();
 | 
						|
}
 | 
						|
 | 
						|
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() {
 | 
						|
	_change(speed());
 | 
						|
	_saved.fire({});
 | 
						|
}
 | 
						|
 | 
						|
void SpeedController::fillMenu(not_null<Ui::DropdownMenu*> menu) {
 | 
						|
	FillSpeedMenu(
 | 
						|
		menu->menu(),
 | 
						|
		_st.menu,
 | 
						|
		_speedChanged.events_starting_with(speed()),
 | 
						|
		[=](float64 speed) { setSpeed(speed); save(); });
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Media::Player
 |