1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "media/player/media_player_float.h"
9 
10 #include <rpl/merge.h>
11 #include "data/data_document.h"
12 #include "data/data_session.h"
13 #include "data/data_media_types.h"
14 #include "history/view/media/history_view_media.h"
15 #include "history/history_item.h"
16 #include "history/history.h"
17 #include "history/view/history_view_element.h"
18 #include "media/audio/media_audio.h"
19 #include "media/streaming/media_streaming_instance.h"
20 #include "media/view/media_view_playback_progress.h"
21 #include "media/player/media_player_instance.h"
22 #include "window/window_session_controller.h"
23 #include "window/section_widget.h"
24 #include "core/application.h"
25 #include "core/core_settings.h"
26 #include "main/main_session.h"
27 #include "main/main_account.h"
28 #include "ui/ui_utility.h"
29 #include "facades.h"
30 #include "styles/style_media_player.h"
31 #include "styles/style_chat.h"
32 
33 #include <QtWidgets/QApplication>
34 
35 namespace Media {
36 namespace Player {
37 
38 using DoubleClickedCallback = Fn<void(not_null<const HistoryItem*>)>;
39 
Float(QWidget * parent,not_null<HistoryItem * > item,Fn<void (bool visible)> toggleCallback,Fn<void (bool closed)> draggedCallback,DoubleClickedCallback doubleClickedCallback)40 Float::Float(
41 	QWidget *parent,
42 	not_null<HistoryItem*> item,
43 	Fn<void(bool visible)> toggleCallback,
44 	Fn<void(bool closed)> draggedCallback,
45 	DoubleClickedCallback doubleClickedCallback)
46 : RpWidget(parent)
47 , _item(item)
48 , _toggleCallback(std::move(toggleCallback))
49 , _draggedCallback(std::move(draggedCallback))
50 , _doubleClickedCallback(std::move(doubleClickedCallback)) {
51 	auto media = _item->media();
52 	Assert(media != nullptr);
53 
54 	auto document = media->document();
55 	Assert(document != nullptr);
56 	Assert(document->isVideoMessage());
57 
58 	auto margin = st::mediaPlayerFloatMargin;
59 	auto size = 2 * margin + st::mediaPlayerFloatSize;
60 	resize(size, size);
61 
62 	prepareShadow();
63 
64 	document->session().data().itemRepaintRequest(
65 	) | rpl::start_with_next([this](auto item) {
66 		if (_item == item) {
67 			repaintItem();
68 		}
69 	}, lifetime());
70 
71 	document->session().data().itemRemoved(
72 	) | rpl::start_with_next([this](auto item) {
73 		if (_item == item) {
74 			detach();
75 		}
76 	}, lifetime());
77 
78 	document->session().account().sessionChanges(
79 	) | rpl::start_with_next([=] {
80 		detach();
81 	}, lifetime());
82 
83 	setCursor(style::cur_pointer);
84 }
85 
mousePressEvent(QMouseEvent * e)86 void Float::mousePressEvent(QMouseEvent *e) {
87 	_down = true;
88 	_downPoint = e->pos();
89 }
90 
mouseMoveEvent(QMouseEvent * e)91 void Float::mouseMoveEvent(QMouseEvent *e) {
92 	if (_down && (e->pos() - _downPoint).manhattanLength() > QApplication::startDragDistance()) {
93 		_down = false;
94 		_drag = true;
95 		_dragLocalPoint = e->pos();
96 	} else if (_drag) {
97 		auto delta = (e->pos() - _dragLocalPoint);
98 		move(pos() + delta);
99 		setOpacity(outRatio());
100 	}
101 }
102 
outRatio() const103 float64 Float::outRatio() const {
104 	auto parent = parentWidget()->rect();
105 	auto min = 1.;
106 	if (x() < parent.x()) {
107 		accumulate_min(min, 1. - (parent.x() - x()) / float64(width()));
108 	}
109 	if (y() < parent.y()) {
110 		accumulate_min(min, 1. - (parent.y() - y()) / float64(height()));
111 	}
112 	if (x() + width() > parent.x() + parent.width()) {
113 		accumulate_min(min, 1. - (x() + width() - parent.x() - parent.width()) / float64(width()));
114 	}
115 	if (y() + height() > parent.y() + parent.height()) {
116 		accumulate_min(min, 1. - (y() + height() - parent.y() - parent.height()) / float64(height()));
117 	}
118 	return std::clamp(min, 0., 1.);
119 }
120 
mouseReleaseEvent(QMouseEvent * e)121 void Float::mouseReleaseEvent(QMouseEvent *e) {
122 	if (base::take(_down) && _item) {
123 		pauseResume();
124 	}
125 	if (_drag) {
126 		finishDrag(outRatio() < 0.5);
127 	}
128 }
129 
finishDrag(bool closed)130 void Float::finishDrag(bool closed) {
131 	_drag = false;
132 	if (_draggedCallback) {
133 		_draggedCallback(closed);
134 	}
135 }
136 
mouseDoubleClickEvent(QMouseEvent * e)137 void Float::mouseDoubleClickEvent(QMouseEvent *e) {
138 	if (_item && _doubleClickedCallback) {
139 		// Handle second click.
140 		pauseResume();
141 		_doubleClickedCallback(_item);
142 	}
143 }
144 
pauseResume()145 void Float::pauseResume() {
146 	if (const auto streamed = getStreamed()) {
147 		if (streamed->paused()) {
148 			streamed->resume();
149 		} else {
150 			streamed->pause();
151 		}
152 	}
153 }
154 
detach()155 void Float::detach() {
156 	if (_item) {
157 		_item = nullptr;
158 		if (_toggleCallback) {
159 			_toggleCallback(false);
160 		}
161 	}
162 }
163 
prepareShadow()164 void Float::prepareShadow() {
165 	auto shadow = QImage(size() * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
166 	shadow.fill(Qt::transparent);
167 	shadow.setDevicePixelRatio(cRetinaFactor());
168 	{
169 		Painter p(&shadow);
170 		PainterHighQualityEnabler hq(p);
171 		p.setPen(Qt::NoPen);
172 		p.setBrush(st::shadowFg);
173 		auto extend = 2 * st::lineWidth;
174 		p.drawEllipse(getInnerRect().marginsAdded(QMargins(extend, extend, extend, extend)));
175 	}
176 	_shadow = Ui::PixmapFromImage(Images::prepareBlur(std::move(shadow)));
177 }
178 
getInnerRect() const179 QRect Float::getInnerRect() const {
180 	auto margin = st::mediaPlayerFloatMargin;
181 	return rect().marginsRemoved(QMargins(margin, margin, margin, margin));
182 }
183 
paintEvent(QPaintEvent * e)184 void Float::paintEvent(QPaintEvent *e) {
185 	Painter p(this);
186 
187 	p.setOpacity(_opacity);
188 	p.drawPixmap(0, 0, _shadow);
189 
190 	if (!fillFrame() && _toggleCallback) {
191 		_toggleCallback(false);
192 	}
193 
194 	auto inner = getInnerRect();
195 	p.drawImage(inner.topLeft(), _frame);
196 
197 	const auto playback = getPlayback();
198 	const auto progress = playback ? playback->value() : 1.;
199 	if (progress > 0.) {
200 		auto pen = st::historyVideoMessageProgressFg->p;
201 		//auto was = p.pen();
202 		pen.setWidth(st::radialLine);
203 		pen.setCapStyle(Qt::RoundCap);
204 		p.setPen(pen);
205 		p.setOpacity(_opacity * st::historyVideoMessageProgressOpacity);
206 
207 		auto from = QuarterArcLength;
208 		auto len = -qRound(FullArcLength * progress);
209 		auto stepInside = st::radialLine / 2;
210 		{
211 			PainterHighQualityEnabler hq(p);
212 			p.drawArc(inner.marginsRemoved(QMargins(stepInside, stepInside, stepInside, stepInside)), from, len);
213 		}
214 
215 		//p.setPen(was);
216 		//p.setOpacity(_opacity);
217 	}
218 }
219 
getStreamed() const220 Streaming::Instance *Float::getStreamed() const {
221 	return instance()->roundVideoStreamed(_item);
222 }
223 
getPlayback() const224 View::PlaybackProgress *Float::getPlayback() const {
225 	return instance()->roundVideoPlayback(_item);
226 }
227 
hasFrame() const228 bool Float::hasFrame() const {
229 	return (getStreamed() != nullptr);
230 }
231 
fillFrame()232 bool Float::fillFrame() {
233 	auto creating = _frame.isNull();
234 	if (creating) {
235 		_frame = QImage(
236 			getInnerRect().size() * cIntRetinaFactor(),
237 			QImage::Format_ARGB32_Premultiplied);
238 		_frame.setDevicePixelRatio(cRetinaFactor());
239 	}
240 	auto frameInner = [&] {
241 		return QRect(QPoint(), _frame.size() / cIntRetinaFactor());
242 	};
243 	if (const auto streamed = getStreamed()) {
244 		auto request = Streaming::FrameRequest::NonStrict();
245 		request.outer = request.resize = _frame.size();
246 		request.radius = ImageRoundRadius::Ellipse;
247 		auto frame = streamed->frame(request);
248 		if (!frame.isNull()) {
249 			_frame.fill(Qt::transparent);
250 
251 			Painter p(&_frame);
252 			PainterHighQualityEnabler hq(p);
253 			p.drawImage(frameInner(), frame);
254 			return true;
255 		}
256 	}
257 	if (creating) {
258 		_frame.fill(Qt::transparent);
259 
260 		Painter p(&_frame);
261 		PainterHighQualityEnabler hq(p);
262 		p.setPen(Qt::NoPen);
263 		p.setBrush(st::imageBg);
264 		p.drawEllipse(frameInner());
265 	}
266 	return false;
267 }
268 
repaintItem()269 void Float::repaintItem() {
270 	update();
271 	if (hasFrame() && _toggleCallback) {
272 		_toggleCallback(true);
273 	}
274 }
275 
276 
277 template <typename ToggleCallback, typename DraggedCallback>
Item(not_null<QWidget * > parent,not_null<HistoryItem * > item,ToggleCallback toggle,DraggedCallback dragged,DoubleClickedCallback doubleClicked)278 FloatController::Item::Item(
279 	not_null<QWidget*> parent,
280 	not_null<HistoryItem*> item,
281 	ToggleCallback toggle,
282 	DraggedCallback dragged,
283 	DoubleClickedCallback doubleClicked)
284 : animationSide(RectPart::Right)
285 , column(Window::Column::Second)
286 , corner(RectPart::TopRight)
287 , widget(
288 	parent,
289 	item,
290 	[=, toggle = std::move(toggle)](bool visible) {
291 		toggle(this, visible);
292 	},
__anone2c2a83a0602(bool closed) 293 	[=, dragged = std::move(dragged)](bool closed) {
294 		dragged(this, closed);
295 	},
296 	std::move(doubleClicked)) {
297 }
298 
FloatController(not_null<FloatDelegate * > delegate)299 FloatController::FloatController(not_null<FloatDelegate*> delegate)
300 : _delegate(delegate)
301 , _parent(_delegate->floatPlayerWidget()) {
302 	subscribe(Media::Player::instance()->trackChangedNotifier(), [=](
303 			AudioMsgId::Type type) {
304 		if (type == AudioMsgId::Type::Voice) {
305 			checkCurrent();
306 		}
307 	});
308 
309 	startDelegateHandling();
310 }
311 
replaceDelegate(not_null<FloatDelegate * > delegate)312 void FloatController::replaceDelegate(not_null<FloatDelegate*> delegate) {
313 	_delegateLifetime.destroy();
314 
315 	_delegate = delegate;
316 	_parent = _delegate->floatPlayerWidget();
317 
318 	startDelegateHandling();
319 
320 	for (const auto &item : _items) {
321 		item->widget->setParent(_parent);
322 	}
323 	checkVisibility();
324 }
325 
startDelegateHandling()326 void FloatController::startDelegateHandling() {
327 	_delegate->floatPlayerCheckVisibilityRequests(
328 	) | rpl::start_with_next([=] {
329 		checkVisibility();
330 	}, _delegateLifetime);
331 
332 	_delegate->floatPlayerHideAllRequests(
333 	) | rpl::start_with_next([=] {
334 		hideAll();
335 	}, _delegateLifetime);
336 
337 	_delegate->floatPlayerShowVisibleRequests(
338 	) | rpl::start_with_next([=] {
339 		showVisible();
340 	}, _delegateLifetime);
341 
342 	_delegate->floatPlayerRaiseAllRequests(
343 	) | rpl::start_with_next([=] {
344 		raiseAll();
345 	}, _delegateLifetime);
346 
347 	_delegate->floatPlayerUpdatePositionsRequests(
348 	) | rpl::start_with_next([=] {
349 		updatePositions();
350 	}, _delegateLifetime);
351 
352 	_delegate->floatPlayerFilterWheelEventRequests(
353 	) | rpl::start_with_next([=](
354 			const FloatDelegate::FloatPlayerFilterWheelEventRequest &request) {
355 		*request.result = filterWheelEvent(request.object, request.event);
356 	}, _delegateLifetime);
357 
358 	_delegate->floatPlayerAreaUpdates(
359 	) | rpl::start_with_next([=] {
360 		checkVisibility();
361 	}, _delegateLifetime);
362 }
363 
checkCurrent()364 void FloatController::checkCurrent() {
365 	const auto state = Media::Player::instance()->current(AudioMsgId::Type::Voice);
366 	const auto audio = state.audio();
367 	const auto fullId = state.contextId();
368 	const auto last = current();
369 	if (last
370 		&& audio
371 		&& !last->widget->detached()
372 		&& (&last->widget->item()->history()->session() == &audio->session())
373 		&& (last->widget->item()->fullId() == fullId)) {
374 		return;
375 	}
376 	if (last) {
377 		last->widget->detach();
378 	}
379 	if (!audio) {
380 		return;
381 	}
382 	if (const auto item = audio->session().data().message(fullId)) {
383 		if (const auto media = item->media()) {
384 			if (const auto document = media->document()) {
385 				if (document->isVideoMessage()) {
386 					create(item);
387 				}
388 			}
389 		}
390 	}
391 }
392 
create(not_null<HistoryItem * > item)393 void FloatController::create(not_null<HistoryItem*> item) {
394 	_items.push_back(std::make_unique<Item>(
395 		_parent,
396 		item,
397 		[=](not_null<Item*> instance, bool visible) {
398 			instance->hiddenByWidget = !visible;
399 			toggle(instance);
400 		},
401 		[=](not_null<Item*> instance, bool closed) {
402 			finishDrag(instance, closed);
403 		},
404 		[=](not_null<const HistoryItem*> item) {
405 			_delegate->floatPlayerDoubleClickEvent(item);
406 		}));
407 	current()->column = Core::App().settings().floatPlayerColumn();
408 	current()->corner = Core::App().settings().floatPlayerCorner();
409 	checkVisibility();
410 }
411 
toggle(not_null<Item * > instance)412 void FloatController::toggle(not_null<Item*> instance) {
413 	auto visible = !instance->hiddenByHistory && !instance->hiddenByWidget && instance->widget->isReady();
414 	if (instance->visible != visible) {
415 		instance->widget->resetMouseState();
416 		instance->visible = visible;
417 		if (!instance->visibleAnimation.animating() && !instance->hiddenByDrag) {
418 			auto finalRect = QRect(getPosition(instance), instance->widget->size());
419 			instance->animationSide = getSide(finalRect.center());
420 		}
421 		instance->visibleAnimation.start([=] {
422 			updatePosition(instance);
423 		}, visible ? 0. : 1., visible ? 1. : 0., st::slideDuration, visible ? anim::easeOutCirc : anim::linear);
424 		updatePosition(instance);
425 	}
426 }
427 
checkVisibility()428 void FloatController::checkVisibility() {
429 	const auto instance = current();
430 	if (!instance) {
431 		return;
432 	}
433 
434 	const auto item = instance->widget->item();
435 	instance->hiddenByHistory = item
436 		? _delegate->floatPlayerIsVisible(item)
437 		: false;
438 	toggle(instance);
439 	updatePosition(instance);
440 }
441 
hideAll()442 void FloatController::hideAll() {
443 	for (const auto &instance : _items) {
444 		instance->widget->hide();
445 	}
446 }
447 
showVisible()448 void FloatController::showVisible() {
449 	for (const auto &instance : _items) {
450 		if (instance->visible) {
451 			instance->widget->show();
452 		}
453 	}
454 }
455 
raiseAll()456 void FloatController::raiseAll() {
457 	for (const auto &instance : _items) {
458 		instance->widget->raise();
459 	}
460 }
461 
updatePositions()462 void FloatController::updatePositions() {
463 	for (const auto &instance : _items) {
464 		updatePosition(instance.get());
465 	}
466 }
467 
filterWheelEvent(not_null<QObject * > object,not_null<QEvent * > event)468 std::optional<bool> FloatController::filterWheelEvent(
469 		not_null<QObject*> object,
470 		not_null<QEvent*> event) {
471 	for (const auto &instance : _items) {
472 		if (instance->widget == object) {
473 			const auto section = _delegate->floatPlayerGetSection(
474 				instance->column);
475 			return section->floatPlayerHandleWheelEvent(event);
476 		}
477 	}
478 	return std::nullopt;
479 }
480 
updatePosition(not_null<Item * > instance)481 void FloatController::updatePosition(not_null<Item*> instance) {
482 	auto visible = instance->visibleAnimation.value(instance->visible ? 1. : 0.);
483 	if (visible == 0. && !instance->visible) {
484 		instance->widget->hide();
485 		if (instance->widget->detached()) {
486 			InvokeQueued(instance->widget, [=] {
487 				remove(instance);
488 			});
489 		}
490 		return;
491 	}
492 
493 	if (!instance->widget->dragged()) {
494 		if (instance->widget->isHidden()) {
495 			instance->widget->show();
496 		}
497 
498 		auto dragged = instance->draggedAnimation.value(1.);
499 		auto position = QPoint();
500 		if (instance->hiddenByDrag) {
501 			instance->widget->setOpacity(instance->widget->countOpacityByParent());
502 			position = getHiddenPosition(instance->dragFrom, instance->widget->size(), instance->animationSide);
503 		} else {
504 			instance->widget->setOpacity(visible * visible);
505 			position = getPosition(instance);
506 			if (visible < 1.) {
507 				auto hiddenPosition = getHiddenPosition(position, instance->widget->size(), instance->animationSide);
508 				position.setX(anim::interpolate(hiddenPosition.x(), position.x(), visible));
509 				position.setY(anim::interpolate(hiddenPosition.y(), position.y(), visible));
510 			}
511 		}
512 		if (dragged < 1.) {
513 			position.setX(anim::interpolate(instance->dragFrom.x(), position.x(), dragged));
514 			position.setY(anim::interpolate(instance->dragFrom.y(), position.y(), dragged));
515 		}
516 		instance->widget->move(position);
517 	}
518 }
519 
getHiddenPosition(QPoint position,QSize size,RectPart side) const520 QPoint FloatController::getHiddenPosition(
521 		QPoint position,
522 		QSize size,
523 		RectPart side) const {
524 	switch (side) {
525 	case RectPart::Left: return { -size.width(), position.y() };
526 	case RectPart::Top: return { position.x(), -size.height() };
527 	case RectPart::Right: return { _parent->width(), position.y() };
528 	case RectPart::Bottom: return { position.x(), _parent->height() };
529 	}
530 	Unexpected("Bad side in MainWidget::getFloatPlayerHiddenPosition().");
531 }
532 
getPosition(not_null<Item * > instance) const533 QPoint FloatController::getPosition(not_null<Item*> instance) const {
534 	const auto section = _delegate->floatPlayerGetSection(instance->column);
535 	const auto rect = section->floatPlayerAvailableRect();
536 	auto position = rect.topLeft();
537 	if (IsBottomCorner(instance->corner)) {
538 		position.setY(position.y() + rect.height() - instance->widget->height());
539 	}
540 	if (IsRightCorner(instance->corner)) {
541 		position.setX(position.x() + rect.width() - instance->widget->width());
542 	}
543 	return _parent->mapFromGlobal(position);
544 }
545 
getSide(QPoint center) const546 RectPart FloatController::getSide(QPoint center) const {
547 	const auto left = std::abs(center.x());
548 	const auto right = std::abs(_parent->width() - center.x());
549 	const auto top = std::abs(center.y());
550 	const auto bottom = std::abs(_parent->height() - center.y());
551 	if (left < right && left < top && left < bottom) {
552 		return RectPart::Left;
553 	} else if (right < top && right < bottom) {
554 		return RectPart::Right;
555 	} else if (top < bottom) {
556 		return RectPart::Top;
557 	}
558 	return RectPart::Bottom;
559 }
560 
remove(not_null<Item * > instance)561 void FloatController::remove(not_null<Item*> instance) {
562 	auto widget = std::move(instance->widget);
563 	auto i = ranges::find_if(_items, [&](auto &item) {
564 		return (item.get() == instance);
565 	});
566 	Assert(i != _items.end());
567 	_items.erase(i);
568 
569 	// ~QWidget() can call HistoryInner::enterEvent() which can
570 	// lead to repaintHistoryItem() and we'll have an instance
571 	// in _items with destroyed widget. So we destroy the
572 	// instance first and only after that destroy the widget.
573 	widget.destroy();
574 }
575 
updateColumnCorner(QPoint center)576 void FloatController::updateColumnCorner(QPoint center) {
577 	Expects(!_items.empty());
578 
579 	auto size = _items.back()->widget->size();
580 	auto min = INT_MAX;
581 	auto column = Core::App().settings().floatPlayerColumn();
582 	auto corner = Core::App().settings().floatPlayerCorner();
583 	auto checkSection = [&](
584 			not_null<FloatSectionDelegate*> widget,
585 			Window::Column widgetColumn) {
586 		auto rect = _parent->mapFromGlobal(
587 			widget->floatPlayerAvailableRect());
588 		auto left = rect.x() + (size.width() / 2);
589 		auto right = rect.x() + rect.width() - (size.width() / 2);
590 		auto top = rect.y() + (size.height() / 2);
591 		auto bottom = rect.y() + rect.height() - (size.height() / 2);
592 		auto checkCorner = [&](QPoint point, RectPart checked) {
593 			auto distance = (point - center).manhattanLength();
594 			if (min > distance) {
595 				min = distance;
596 				column = widgetColumn;
597 				corner = checked;
598 			}
599 		};
600 		checkCorner({ left, top }, RectPart::TopLeft);
601 		checkCorner({ right, top }, RectPart::TopRight);
602 		checkCorner({ left, bottom }, RectPart::BottomLeft);
603 		checkCorner({ right, bottom }, RectPart::BottomRight);
604 	};
605 
606 	_delegate->floatPlayerEnumerateSections(checkSection);
607 
608 	auto &settings = Core::App().settings();
609 	if (settings.floatPlayerColumn() != column) {
610 		settings.setFloatPlayerColumn(column);
611 		Core::App().saveSettingsDelayed();
612 	}
613 	if (settings.floatPlayerCorner() != corner) {
614 		settings.setFloatPlayerCorner(corner);
615 		Core::App().saveSettingsDelayed();
616 	}
617 }
618 
finishDrag(not_null<Item * > instance,bool closed)619 void FloatController::finishDrag(not_null<Item*> instance, bool closed) {
620 	instance->dragFrom = instance->widget->pos();
621 	const auto center = instance->widget->geometry().center();
622 	if (closed) {
623 		instance->hiddenByDrag = true;
624 		instance->animationSide = getSide(center);
625 	}
626 	updateColumnCorner(center);
627 	instance->column = Core::App().settings().floatPlayerColumn();
628 	instance->corner = Core::App().settings().floatPlayerCorner();
629 
630 	instance->draggedAnimation.stop();
631 	instance->draggedAnimation.start(
632 		[=] { updatePosition(instance); },
633 		0.,
634 		1.,
635 		st::slideDuration,
636 		anim::sineInOut);
637 	updatePosition(instance);
638 
639 	if (closed) {
640 		if (const auto item = instance->widget->item()) {
641 			_closeEvents.fire(item->fullId());
642 		}
643 		instance->widget->detach();
644 		Media::Player::instance()->stop(AudioMsgId::Type::Voice);
645 	}
646 }
647 
648 } // namespace Player
649 } // namespace Media
650