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_panel.h"
9 
10 #include "media/player/media_player_instance.h"
11 #include "info/media/info_media_list_widget.h"
12 #include "history/history.h"
13 #include "history/history_item.h"
14 #include "data/data_session.h"
15 #include "data/data_document.h"
16 #include "data/data_media_types.h"
17 #include "data/data_channel.h"
18 #include "data/data_chat.h"
19 #include "ui/widgets/shadow.h"
20 #include "ui/widgets/scroll_area.h"
21 #include "ui/ui_utility.h"
22 #include "ui/cached_round_corners.h"
23 #include "mainwindow.h"
24 #include "main/main_session.h"
25 #include "styles/style_overview.h"
26 #include "styles/style_widgets.h"
27 #include "styles/style_media_player.h"
28 #include "styles/style_info.h"
29 
30 namespace Media {
31 namespace Player {
32 namespace {
33 
34 using ListWidget = Info::Media::ListWidget;
35 
36 constexpr auto kPlaylistIdsLimit = 32;
37 constexpr auto kDelayedHideTimeout = crl::time(3000);
38 
39 } // namespace
40 
Panel(QWidget * parent,not_null<Window::SessionController * > window)41 Panel::Panel(
42 	QWidget *parent,
43 	not_null<Window::SessionController*> window)
44 : RpWidget(parent)
45 , AbstractController(window)
46 , _showTimer([=] { startShow(); })
__anon04948e540302null47 , _hideTimer([=] { startHideChecked(); })
48 , _scroll(this, st::mediaPlayerScroll) {
49 	hide();
50 	updateSize();
51 }
52 
overlaps(const QRect & globalRect)53 bool Panel::overlaps(const QRect &globalRect) {
54 	if (isHidden() || _a_appearance.animating()) return false;
55 
56 	auto marginLeft = rtl() ? contentRight() : contentLeft();
57 	auto marginRight = rtl() ? contentLeft() : contentRight();
58 	return rect().marginsRemoved(QMargins(marginLeft, contentTop(), marginRight, contentBottom())).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
59 }
60 
resizeEvent(QResizeEvent * e)61 void Panel::resizeEvent(QResizeEvent *e) {
62 	updateControlsGeometry();
63 }
64 
listHeightUpdated(int newHeight)65 void Panel::listHeightUpdated(int newHeight) {
66 	if (newHeight > emptyInnerHeight()) {
67 		updateSize();
68 	} else {
69 		_hideTimer.callOnce(0);
70 	}
71 }
72 
contentTooSmall() const73 bool Panel::contentTooSmall() const {
74 	const auto innerHeight = _scroll->widget()
75 		? _scroll->widget()->height()
76 		: emptyInnerHeight();
77 	return (innerHeight <= emptyInnerHeight());
78 }
79 
emptyInnerHeight() const80 int Panel::emptyInnerHeight() const {
81 	return st::infoMediaMargin.top()
82 		+ st::overviewFileLayout.songPadding.top()
83 		+ st::overviewFileLayout.songThumbSize
84 		+ st::overviewFileLayout.songPadding.bottom()
85 		+ st::infoMediaMargin.bottom();
86 }
87 
preventAutoHide() const88 bool Panel::preventAutoHide() const {
89 	if (const auto list = static_cast<ListWidget*>(_scroll->widget())) {
90 		return list->preventAutoHide();
91 	}
92 	return false;
93 }
94 
updateControlsGeometry()95 void Panel::updateControlsGeometry() {
96 	const auto scrollTop = contentTop();
97 	const auto width = contentWidth();
98 	const auto scrollHeight = qMax(
99 		height() - scrollTop - contentBottom() - scrollMarginBottom(),
100 		0);
101 	if (scrollHeight > 0) {
102 		_scroll->setGeometryToRight(contentRight(), scrollTop, width, scrollHeight);
103 	}
104 	if (const auto widget = static_cast<TWidget*>(_scroll->widget())) {
105 		widget->resizeToWidth(width);
106 	}
107 }
108 
bestPositionFor(int left) const109 int Panel::bestPositionFor(int left) const {
110 	left -= contentLeft();
111 	left -= st::mediaPlayerFileLayout.songPadding.left();
112 	left -= st::mediaPlayerFileLayout.songThumbSize / 2;
113 	return left;
114 }
115 
scrollPlaylistToCurrentTrack()116 void Panel::scrollPlaylistToCurrentTrack() {
117 	if (const auto list = static_cast<ListWidget*>(_scroll->widget())) {
118 		const auto rect = list->getCurrentSongGeometry();
119 		_scroll->scrollToY(rect.y() - st::infoMediaMargin.top());
120 	}
121 }
122 
updateSize()123 void Panel::updateSize() {
124 	auto width = contentLeft() + st::mediaPlayerPanelWidth + contentRight();
125 	auto height = contentTop();
126 	auto listHeight = 0;
127 	if (auto widget = _scroll->widget()) {
128 		listHeight = widget->height();
129 	}
130 	auto scrollVisible = (listHeight > 0);
131 	auto scrollHeight = scrollVisible ? (qMin(listHeight, st::mediaPlayerListHeightMax) + st::mediaPlayerListMarginBottom) : 0;
132 	height += scrollHeight + contentBottom();
133 	resize(width, height);
134 	_scroll->setVisible(scrollVisible);
135 }
136 
paintEvent(QPaintEvent * e)137 void Panel::paintEvent(QPaintEvent *e) {
138 	Painter p(this);
139 
140 	if (!_cache.isNull()) {
141 		bool animating = _a_appearance.animating();
142 		if (animating) {
143 			p.setOpacity(_a_appearance.value(_hiding ? 0. : 1.));
144 		} else if (_hiding || isHidden()) {
145 			hideFinished();
146 			return;
147 		}
148 		p.drawPixmap(0, 0, _cache);
149 		if (!animating) {
150 			showChildren();
151 			_cache = QPixmap();
152 		}
153 		return;
154 	}
155 
156 	// draw shadow
157 	auto shadowedRect = myrtlrect(contentLeft(), contentTop(), contentWidth(), contentHeight());
158 	auto shadowedSides = (rtl() ? RectPart::Right : RectPart::Left)
159 		| RectPart::Bottom
160 		| (rtl() ? RectPart::Left : RectPart::Right)
161 		| RectPart::Top;
162 	Ui::Shadow::paint(p, shadowedRect, width(), st::defaultRoundShadow, shadowedSides);
163 	auto parts = RectPart::Full;
164 	Ui::FillRoundRect(p, shadowedRect, st::menuBg, Ui::MenuCorners, nullptr, parts);
165 }
166 
enterEventHook(QEnterEvent * e)167 void Panel::enterEventHook(QEnterEvent *e) {
168 	if (_ignoringEnterEvents || contentTooSmall()) return;
169 
170 	_hideTimer.cancel();
171 	if (_a_appearance.animating()) {
172 		startShow();
173 	} else {
174 		_showTimer.callOnce(0);
175 	}
176 	return RpWidget::enterEventHook(e);
177 }
178 
leaveEventHook(QEvent * e)179 void Panel::leaveEventHook(QEvent *e) {
180 	if (preventAutoHide()) {
181 		return;
182 	}
183 	_showTimer.cancel();
184 	if (_a_appearance.animating()) {
185 		startHide();
186 	} else {
187 		_hideTimer.callOnce(300);
188 	}
189 	return RpWidget::leaveEventHook(e);
190 }
191 
showFromOther()192 void Panel::showFromOther() {
193 	_hideTimer.cancel();
194 	if (_a_appearance.animating()) {
195 		startShow();
196 	} else {
197 		_showTimer.callOnce(300);
198 	}
199 }
200 
hideFromOther()201 void Panel::hideFromOther() {
202 	_showTimer.cancel();
203 	if (_a_appearance.animating()) {
204 		startHide();
205 	} else {
206 		_hideTimer.callOnce(0);
207 	}
208 }
209 
ensureCreated()210 void Panel::ensureCreated() {
211 	if (_scroll->widget()) return;
212 
213 	_refreshListLifetime = instance()->playlistChanges(
214 		AudioMsgId::Type::Song
215 	) | rpl::start_with_next([=] {
216 		refreshList();
217 	});
218 	refreshList();
219 
220 	macWindowDeactivateEvents(
221 	) | rpl::filter([=] {
222 		return !isHidden();
223 	}) | rpl::start_with_next([=] {
224 		leaveEvent(nullptr);
225 	}, _refreshListLifetime);
226 
227 	_ignoringEnterEvents = false;
228 }
229 
refreshList()230 void Panel::refreshList() {
231 	const auto current = instance()->current(AudioMsgId::Type::Song);
232 	const auto contextId = current.contextId();
233 	const auto peer = [&]() -> PeerData* {
234 		if (const auto document = current.audio()) {
235 			if (&document->session() != &session()) {
236 				// Different account is playing music.
237 				return nullptr;
238 			}
239 		}
240 		const auto item = contextId
241 			? session().data().message(contextId)
242 			: nullptr;
243 		const auto media = item ? item->media() : nullptr;
244 		const auto document = media ? media->document() : nullptr;
245 		if (!document
246 			|| !document->isSharedMediaMusic()
247 			|| (!item->isRegular() && !item->isScheduled())) {
248 			return nullptr;
249 		}
250 		const auto result = item->history()->peer;
251 		if (const auto migrated = result->migrateTo()) {
252 			return migrated;
253 		}
254 		return result;
255 	}();
256 	const auto migrated = peer ? peer->migrateFrom() : nullptr;
257 	if (_listPeer != peer || _listMigratedPeer != migrated) {
258 		_scroll->takeWidget<QWidget>().destroy();
259 		_listPeer = _listMigratedPeer = nullptr;
260 	}
261 	if (peer && !_listPeer) {
262 		_listPeer = peer;
263 		_listMigratedPeer = migrated;
264 		auto list = object_ptr<ListWidget>(this, infoController());
265 
266 		const auto weak = _scroll->setOwnedWidget(std::move(list));
267 
268 		updateSize();
269 		updateControlsGeometry();
270 
271 		weak->checkForHide(
272 		) | rpl::start_with_next([this] {
273 			if (!rect().contains(mapFromGlobal(QCursor::pos()))) {
274 				_hideTimer.callOnce(kDelayedHideTimeout);
275 			}
276 		}, weak->lifetime());
277 
278 		weak->heightValue(
279 		) | rpl::start_with_next([this](int newHeight) {
280 			listHeightUpdated(newHeight);
281 		}, weak->lifetime());
282 
283 		weak->scrollToRequests(
284 		) | rpl::start_with_next([this](int newScrollTop) {
285 			_scroll->scrollToY(newScrollTop);
286 		}, weak->lifetime());
287 
288 		// MSVC BUG + REGRESSION rpl::mappers::tuple :(
289 		using namespace rpl::mappers;
290 		rpl::combine(
291 			_scroll->scrollTopValue(),
292 			_scroll->heightValue()
293 		) | rpl::start_with_next([=](int top, int height) {
294 			const auto bottom = top + height;
295 			weak->setVisibleTopBottom(top, bottom);
296 		}, weak->lifetime());
297 
298 		auto memento = Info::Media::Memento(
299 			peer,
300 			migratedPeerId(),
301 			section().mediaType());
302 		memento.setAroundId(contextId);
303 		memento.setIdsLimit(kPlaylistIdsLimit);
304 		memento.setScrollTopItem(contextId);
305 		memento.setScrollTopShift(-st::infoMediaMargin.top());
306 		weak->restoreState(&memento);
307 	}
308 }
309 
performDestroy()310 void Panel::performDestroy() {
311 	if (!_scroll->widget()) return;
312 
313 	_scroll->takeWidget<QWidget>().destroy();
314 	_listPeer = _listMigratedPeer = nullptr;
315 	_refreshListLifetime.destroy();
316 }
317 
key() const318 Info::Key Panel::key() const {
319 	return Info::Key(_listPeer);
320 }
321 
migrated() const322 PeerData *Panel::migrated() const {
323 	return _listMigratedPeer;
324 }
325 
section() const326 Info::Section Panel::section() const {
327 	return Info::Section(Info::Section::MediaType::MusicFile);
328 }
329 
startShow()330 void Panel::startShow() {
331 	ensureCreated();
332 	if (contentTooSmall()) {
333 		return;
334 	}
335 
336 	if (isHidden()) {
337 		scrollPlaylistToCurrentTrack();
338 		show();
339 	} else if (!_hiding) {
340 		return;
341 	}
342 	_hiding = false;
343 	startAnimation();
344 }
345 
hideIgnoringEnterEvents()346 void Panel::hideIgnoringEnterEvents() {
347 	_ignoringEnterEvents = true;
348 	if (isHidden()) {
349 		hideFinished();
350 	} else {
351 		startHide();
352 	}
353 }
354 
startHideChecked()355 void Panel::startHideChecked() {
356 	if (!contentTooSmall() && preventAutoHide()) {
357 		return;
358 	}
359 	if (isHidden()) {
360 		hideFinished();
361 	} else {
362 		startHide();
363 	}
364 }
365 
startHide()366 void Panel::startHide() {
367 	if (_hiding || isHidden()) return;
368 
369 	_hiding = true;
370 	startAnimation();
371 }
372 
startAnimation()373 void Panel::startAnimation() {
374 	auto from = _hiding ? 1. : 0.;
375 	auto to = _hiding ? 0. : 1.;
376 	if (_cache.isNull()) {
377 		showChildren();
378 		_cache = Ui::GrabWidget(this);
379 	}
380 	hideChildren();
381 	_a_appearance.start([this] { appearanceCallback(); }, from, to, st::defaultInnerDropdown.duration);
382 }
383 
appearanceCallback()384 void Panel::appearanceCallback() {
385 	if (!_a_appearance.animating() && _hiding) {
386 		_hiding = false;
387 		hideFinished();
388 	} else {
389 		update();
390 	}
391 }
392 
hideFinished()393 void Panel::hideFinished() {
394 	hide();
395 	_cache = QPixmap();
396 	performDestroy();
397 }
398 
contentLeft() const399 int Panel::contentLeft() const {
400 	return st::mediaPlayerPanelMarginLeft;
401 }
402 
contentTop() const403 int Panel::contentTop() const {
404 	return st::mediaPlayerPanelMarginLeft;
405 }
406 
contentRight() const407 int Panel::contentRight() const {
408 	return st::mediaPlayerPanelMarginLeft;
409 }
410 
contentBottom() const411 int Panel::contentBottom() const {
412 	return st::mediaPlayerPanelMarginBottom;
413 }
414 
scrollMarginBottom() const415 int Panel::scrollMarginBottom() const {
416 	return 0;// st::mediaPlayerPanelMarginBottom;
417 }
418 
419 } // namespace Player
420 } // namespace Media
421