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/view/media_view_overlay_widget.h"
9 
10 #include "apiwrap.h"
11 #include "api/api_attached_stickers.h"
12 #include "api/api_peer_photo.h"
13 #include "lang/lang_keys.h"
14 #include "mainwindow.h"
15 #include "core/application.h"
16 #include "core/click_handler_types.h"
17 #include "core/file_utilities.h"
18 #include "core/mime_type.h"
19 #include "core/ui_integration.h"
20 #include "core/crash_reports.h"
21 #include "ui/widgets/popup_menu.h"
22 #include "ui/widgets/buttons.h"
23 #include "ui/image/image.h"
24 #include "ui/text/text_utilities.h"
25 #include "ui/platform/ui_platform_utility.h"
26 #include "ui/toast/toast.h"
27 #include "ui/text/format_values.h"
28 #include "ui/item_text_options.h"
29 #include "ui/ui_utility.h"
30 #include "ui/cached_round_corners.h"
31 #include "ui/gl/gl_surface.h"
32 #include "ui/boxes/confirm_box.h"
33 #include "boxes/delete_messages_box.h"
34 #include "media/audio/media_audio.h"
35 #include "media/view/media_view_playback_controls.h"
36 #include "media/view/media_view_group_thumbs.h"
37 #include "media/view/media_view_pip.h"
38 #include "media/view/media_view_overlay_raster.h"
39 #include "media/view/media_view_overlay_opengl.h"
40 #include "media/streaming/media_streaming_instance.h"
41 #include "media/streaming/media_streaming_player.h"
42 #include "media/player/media_player_instance.h"
43 #include "history/history.h"
44 #include "history/history_message.h"
45 #include "history/view/media/history_view_media.h"
46 #include "data/data_media_types.h"
47 #include "data/data_session.h"
48 #include "data/data_channel.h"
49 #include "data/data_chat.h"
50 #include "data/data_user.h"
51 #include "data/data_file_origin.h"
52 #include "data/data_media_rotation.h"
53 #include "data/data_photo_media.h"
54 #include "data/data_document_media.h"
55 #include "data/data_document_resolver.h"
56 #include "data/data_file_click_handler.h"
57 #include "window/themes/window_theme_preview.h"
58 #include "window/window_peer_menu.h"
59 #include "window/window_session_controller.h"
60 #include "window/window_controller.h"
61 #include "base/platform/base_platform_info.h"
62 #include "base/random.h"
63 #include "base/unixtime.h"
64 #include "base/qt_signal_producer.h"
65 #include "base/event_filter.h"
66 #include "main/main_account.h"
67 #include "main/main_domain.h" // Domain::activeSessionValue.
68 #include "main/main_session.h"
69 #include "main/main_session_settings.h"
70 #include "layout/layout_document_generic_preview.h"
71 #include "storage/file_download.h"
72 #include "storage/storage_account.h"
73 #include "calls/calls_instance.h"
74 #include "facades.h"
75 #include "styles/style_media_view.h"
76 #include "styles/style_chat.h"
77 #include "base/qt_adapters.h"
78 
79 #ifdef Q_OS_MAC
80 #include "platform/mac/touchbar/mac_touchbar_media_view.h"
81 #endif // Q_OS_MAC
82 
83 #include <QtWidgets/QApplication>
84 #include <QtCore/QBuffer>
85 #include <QtGui/QGuiApplication>
86 #include <QtGui/QClipboard>
87 #include <QtGui/QWindow>
88 #include <QtGui/QScreen>
89 
90 namespace Media {
91 namespace View {
92 namespace {
93 
94 constexpr auto kPreloadCount = 3;
95 constexpr auto kMaxZoomLevel = 7; // x8
96 constexpr auto kZoomToScreenLevel = 1024;
97 constexpr auto kOverlayLoaderPriority = 2;
98 constexpr auto kSeekTimeMs = 5 * crl::time(1000);
99 
100 // macOS OpenGL renderer fails to render larger texture
101 // even though it reports that max texture size is 16384.
102 constexpr auto kMaxDisplayImageSize = 4096;
103 
104 // Preload X message ids before and after current.
105 constexpr auto kIdsLimit = 48;
106 
107 // Preload next messages if we went further from current than that.
108 constexpr auto kIdsPreloadAfter = 28;
109 
110 class PipDelegate final : public Pip::Delegate {
111 public:
112 	PipDelegate(QWidget *parent, not_null<Main::Session*> session);
113 
114 	void pipSaveGeometry(QByteArray geometry) override;
115 	QByteArray pipLoadGeometry() override;
116 	float64 pipPlaybackSpeed() override;
117 	QWidget *pipParentWidget() override;
118 
119 private:
120 	QWidget *_parent = nullptr;
121 	not_null<Main::Session*> _session;
122 
123 };
124 
PipDelegate(QWidget * parent,not_null<Main::Session * > session)125 PipDelegate::PipDelegate(QWidget *parent, not_null<Main::Session*> session)
126 : _parent(parent)
127 , _session(session) {
128 }
129 
pipSaveGeometry(QByteArray geometry)130 void PipDelegate::pipSaveGeometry(QByteArray geometry) {
131 	Core::App().settings().setVideoPipGeometry(geometry);
132 	Core::App().saveSettingsDelayed();
133 }
134 
pipLoadGeometry()135 QByteArray PipDelegate::pipLoadGeometry() {
136 	return Core::App().settings().videoPipGeometry();
137 }
138 
pipPlaybackSpeed()139 float64 PipDelegate::pipPlaybackSpeed() {
140 	return Core::App().settings().videoPlaybackSpeed();
141 }
142 
pipParentWidget()143 QWidget *PipDelegate::pipParentWidget() {
144 	return _parent;
145 }
146 
VideoThumbOptions(DocumentData * document)147 [[nodiscard]] Images::Options VideoThumbOptions(DocumentData *document) {
148 	const auto result = Images::Option::Smooth | Images::Option::Blurred;
149 	return (document && document->isVideoMessage())
150 		? (result | Images::Option::Circled)
151 		: result;
152 }
153 
PrepareStaticImage(Images::ReadArgs && args)154 [[nodiscard]] QImage PrepareStaticImage(Images::ReadArgs &&args) {
155 	auto read = Images::Read(std::move(args));
156 	return (read.image.width() > kMaxDisplayImageSize
157 		|| read.image.height() > kMaxDisplayImageSize)
158 		? read.image.scaled(
159 			kMaxDisplayImageSize,
160 			kMaxDisplayImageSize,
161 			Qt::KeepAspectRatio,
162 			Qt::SmoothTransformation)
163 		: read.image;
164 }
165 
IsSemitransparent(const QImage & image)166 [[nodiscard]] bool IsSemitransparent(const QImage &image) {
167 	if (image.isNull()) {
168 		return true;
169 	} else if (!image.hasAlphaChannel()) {
170 		return false;
171 	}
172 	Assert(image.format() == QImage::Format_ARGB32_Premultiplied);
173 	constexpr auto kAlphaMask = 0xFF000000;
174 	auto ints = reinterpret_cast<const uint32*>(image.bits());
175 	const auto add = (image.bytesPerLine() / 4) - image.width();
176 	for (auto y = 0; y != image.height(); ++y) {
177 		for (auto till = ints + image.width(); ints != till; ++ints) {
178 			if ((*ints & kAlphaMask) != kAlphaMask) {
179 				return true;
180 			}
181 		}
182 		ints += add;
183 	}
184 	return false;
185 }
186 
187 } // namespace
188 
189 struct OverlayWidget::SharedMedia {
SharedMediaMedia::View::OverlayWidget::SharedMedia190 	SharedMedia(SharedMediaKey key) : key(key) {
191 	}
192 
193 	SharedMediaKey key;
194 	rpl::lifetime lifetime;
195 };
196 
197 struct OverlayWidget::UserPhotos {
UserPhotosMedia::View::OverlayWidget::UserPhotos198 	UserPhotos(UserPhotosKey key) : key(key) {
199 	}
200 
201 	UserPhotosKey key;
202 	rpl::lifetime lifetime;
203 };
204 
205 struct OverlayWidget::Collage {
CollageMedia::View::OverlayWidget::Collage206 	Collage(CollageKey key) : key(key) {
207 	}
208 
209 	CollageKey key;
210 };
211 
212 struct OverlayWidget::Streamed {
213 	Streamed(
214 		not_null<DocumentData*> document,
215 		Data::FileOrigin origin,
216 		not_null<QWidget*> controlsParent,
217 		not_null<PlaybackControls::Delegate*> controlsDelegate,
218 		Fn<void()> waitingCallback);
219 	Streamed(
220 		not_null<PhotoData*> photo,
221 		Data::FileOrigin origin,
222 		not_null<QWidget*> controlsParent,
223 		not_null<PlaybackControls::Delegate*> controlsDelegate,
224 		Fn<void()> waitingCallback);
225 
226 	Streaming::Instance instance;
227 	PlaybackControls controls;
228 
229 	bool withSound = false;
230 	bool pausedBySeek = false;
231 	bool resumeOnCallEnd = false;
232 };
233 
234 struct OverlayWidget::PipWrap {
235 	PipWrap(
236 		QWidget *parent,
237 		not_null<DocumentData*> document,
238 		FullMsgId contextId,
239 		std::shared_ptr<Streaming::Document> shared,
240 		FnMut<void()> closeAndContinue,
241 		FnMut<void()> destroy);
242 
243 	PipWrap(const PipWrap &other) = delete;
244 	PipWrap &operator=(const PipWrap &other) = delete;
245 
246 	PipDelegate delegate;
247 	Pip wrapped;
248 };
249 
Streamed(not_null<DocumentData * > document,Data::FileOrigin origin,not_null<QWidget * > controlsParent,not_null<PlaybackControls::Delegate * > controlsDelegate,Fn<void ()> waitingCallback)250 OverlayWidget::Streamed::Streamed(
251 	not_null<DocumentData*> document,
252 	Data::FileOrigin origin,
253 	not_null<QWidget*> controlsParent,
254 	not_null<PlaybackControls::Delegate*> controlsDelegate,
255 	Fn<void()> waitingCallback)
256 : instance(document, origin, std::move(waitingCallback))
257 , controls(controlsParent, controlsDelegate) {
258 }
259 
Streamed(not_null<PhotoData * > photo,Data::FileOrigin origin,not_null<QWidget * > controlsParent,not_null<PlaybackControls::Delegate * > controlsDelegate,Fn<void ()> waitingCallback)260 OverlayWidget::Streamed::Streamed(
261 	not_null<PhotoData*> photo,
262 	Data::FileOrigin origin,
263 	not_null<QWidget*> controlsParent,
264 	not_null<PlaybackControls::Delegate*> controlsDelegate,
265 	Fn<void()> waitingCallback)
266 : instance(photo, origin, std::move(waitingCallback))
267 , controls(controlsParent, controlsDelegate) {
268 }
269 
PipWrap(QWidget * parent,not_null<DocumentData * > document,FullMsgId contextId,std::shared_ptr<Streaming::Document> shared,FnMut<void ()> closeAndContinue,FnMut<void ()> destroy)270 OverlayWidget::PipWrap::PipWrap(
271 	QWidget *parent,
272 	not_null<DocumentData*> document,
273 	FullMsgId contextId,
274 	std::shared_ptr<Streaming::Document> shared,
275 	FnMut<void()> closeAndContinue,
276 	FnMut<void()> destroy)
277 : delegate(parent, &document->session())
278 , wrapped(
279 	&delegate,
280 	document,
281 	contextId,
282 	std::move(shared),
283 	std::move(closeAndContinue),
284 	std::move(destroy)) {
285 }
286 
OverlayWidget()287 OverlayWidget::OverlayWidget()
288 : _surface(Ui::GL::CreateSurface(
289 	[=](Ui::GL::Capabilities capabilities) {
290 		return chooseRenderer(capabilities);
291 	}))
292 , _widget(_surface->rpWidget())
293 , _docDownload(_widget, tr::lng_media_download(tr::now), st::mediaviewFileLink)
294 , _docSaveAs(_widget, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink)
295 , _docCancel(_widget, tr::lng_cancel(tr::now), st::mediaviewFileLink)
__anon022521c50302(crl::time now) 296 , _radial([=](crl::time now) { return radialAnimationCallback(now); })
297 , _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction)
__anon022521c50402(crl::time now) 298 , _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); })
299 , _dropdown(_widget, st::mediaviewDropdownMenu) {
300 	CrashReports::SetAnnotation("OpenGL Renderer", "[not-initialized]");
301 
302 	Lang::Updated(
__anon022521c50502null303 	) | rpl::start_with_next([=] {
304 		refreshLang();
305 	}, lifetime());
306 
307 	_lastPositiveVolume = (Core::App().settings().videoVolume() > 0.)
308 		? Core::App().settings().videoVolume()
309 		: Core::Settings::kDefaultVolume;
310 
311 	_widget->setWindowTitle(qsl("Media viewer"));
312 
313 	const auto text = tr::lng_mediaview_saved_to(
314 		tr::now,
315 		lt_downloads,
316 		Ui::Text::Link(
317 			tr::lng_mediaview_downloads(tr::now),
318 			"internal:show_saved_message"),
319 		Ui::Text::WithEntities);
320 	_saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text, Ui::DialogTextOptions());
321 	_saveMsg = QRect(0, 0, _saveMsgText.maxWidth() + st::mediaviewSaveMsgPadding.left() + st::mediaviewSaveMsgPadding.right(), st::mediaviewSaveMsgStyle.font->height + st::mediaviewSaveMsgPadding.top() + st::mediaviewSaveMsgPadding.bottom());
322 	_saveMsgImage = QImage(
323 		_saveMsg.size() * cIntRetinaFactor(),
324 		QImage::Format_ARGB32_Premultiplied);
325 
326 	_docRectImage = QImage(
327 		st::mediaviewFileSize * cIntRetinaFactor(),
328 		QImage::Format_ARGB32_Premultiplied);
329 	_docRectImage.setDevicePixelRatio(cIntRetinaFactor());
330 
331 	_surface->shownValue(
__anon022521c50602(bool shown) 332 	) | rpl::start_with_next([=](bool shown) {
333 		toggleApplicationEventFilter(shown);
334 		if (shown) {
335 			const auto screenList = QGuiApplication::screens();
336 			DEBUG_LOG(("Viewer Pos: Shown, screen number: %1")
337 				.arg(screenList.indexOf(window()->screen())));
338 			moveToScreen();
339 		} else {
340 			clearAfterHide();
341 		}
342 	}, lifetime());
343 
__anon022521c50702(not_null<QEvent*> e) 344 	const auto mousePosition = [](not_null<QEvent*> e) {
345 		return static_cast<QMouseEvent*>(e.get())->pos();
346 	};
__anon022521c50802(not_null<QEvent*> e) 347 	const auto mouseButton = [](not_null<QEvent*> e) {
348 		return static_cast<QMouseEvent*>(e.get())->button();
349 	};
__anon022521c50902(not_null<QEvent*> e) 350 	base::install_event_filter(_widget, [=](not_null<QEvent*> e) {
351 		const auto type = e->type();
352 		if (type == QEvent::Move) {
353 			const auto position = static_cast<QMoveEvent*>(e.get())->pos();
354 			DEBUG_LOG(("Viewer Pos: Moved to %1, %2")
355 				.arg(position.x())
356 				.arg(position.y()));
357 		} else if (type == QEvent::Resize) {
358 			const auto size = static_cast<QResizeEvent*>(e.get())->size();
359 			DEBUG_LOG(("Viewer Pos: Resized to %1, %2")
360 				.arg(size.width())
361 				.arg(size.height()));
362 			updateControlsGeometry();
363 		} else if (type == QEvent::MouseButtonPress) {
364 			handleMousePress(mousePosition(e), mouseButton(e));
365 		} else if (type == QEvent::MouseButtonRelease) {
366 			handleMouseRelease(mousePosition(e), mouseButton(e));
367 		} else if (type == QEvent::MouseMove) {
368 			handleMouseMove(mousePosition(e));
369 		} else if (type == QEvent::KeyPress) {
370 			handleKeyPress(static_cast<QKeyEvent*>(e.get()));
371 		} else if (type == QEvent::ContextMenu) {
372 			const auto event = static_cast<QContextMenuEvent*>(e.get());
373 			const auto mouse = (event->reason() == QContextMenuEvent::Mouse);
374 			const auto position = mouse
375 				? std::make_optional(event->pos())
376 				: std::nullopt;
377 			if (handleContextMenu(position)) {
378 				return base::EventFilterResult::Cancel;
379 			}
380 		} else if (type == QEvent::MouseButtonDblClick) {
381 			if (handleDoubleClick(mousePosition(e), mouseButton(e))) {
382 				return base::EventFilterResult::Cancel;
383 			} else {
384 				handleMousePress(mousePosition(e), mouseButton(e));
385 			}
386 		} else if (type == QEvent::TouchBegin
387 			|| type == QEvent::TouchUpdate
388 			|| type == QEvent::TouchEnd
389 			|| type == QEvent::TouchCancel) {
390 			if (handleTouchEvent(static_cast<QTouchEvent*>(e.get()))) {
391 				return base::EventFilterResult::Cancel;;
392 			}
393 		} else if (type == QEvent::Wheel) {
394 			handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
395 		}
396 		return base::EventFilterResult::Continue;
397 	});
398 
399 	if (Platform::IsLinux()) {
400 		_widget->setWindowFlags(Qt::FramelessWindowHint
401 			| Qt::MaximizeUsingFullscreenGeometryHint);
402 	} else if (Platform::IsMac()) {
403 		// Without Qt::Tool starting with Qt 5.15.1 this widget
404 		// when being opened from a fullscreen main window was
405 		// opening not as overlay over the main window, but as
406 		// a separate fullscreen window with a separate space.
407 		_widget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);
408 	} else {
409 		_widget->setWindowFlags(Qt::FramelessWindowHint);
410 	}
411 	_widget->setAttribute(Qt::WA_NoSystemBackground, true);
412 	_widget->setAttribute(Qt::WA_TranslucentBackground, true);
413 	_widget->setMouseTracking(true);
414 
415 	hide();
416 	_widget->createWinId();
417 	if (Platform::IsLinux()) {
418 		window()->setTransientParent(App::wnd()->windowHandle());
419 		_widget->setWindowModality(Qt::WindowModal);
420 	}
421 	if (!Platform::IsMac()) {
422 		_widget->setWindowState(Qt::WindowFullScreen);
423 	}
424 
425 	QObject::connect(
426 		window(),
427 		&QWindow::screenChanged,
__anon022521c50a02(QScreen *screen) 428 		[=](QScreen *screen) { handleScreenChanged(screen); });
429 	subscribeToScreenGeometry();
430 	updateGeometry();
431 	updateControlsGeometry();
432 
433 #ifdef Q_OS_MAC
434 	TouchBar::SetupMediaViewTouchBar(
435 		_widget->winId(),
436 		static_cast<PlaybackControls::Delegate*>(this),
437 		_touchbarTrackState.events(),
438 		_touchbarDisplay.events(),
439 		_touchbarFullscreenToggled.events());
440 #endif // Q_OS_MAC
441 
442 	using namespace rpl::mappers;
443 	rpl::combine(
444 		Core::App().calls().currentCallValue(),
445 		Core::App().calls().currentGroupCallValue(),
446 		_1 || _2
__anon022521c50b02(bool call) 447 	) | rpl::start_with_next([=](bool call) {
448 		if (!_streamed || videoIsGifOrUserpic()) {
449 			return;
450 		} else if (call) {
451 			playbackPauseOnCall();
452 		} else {
453 			playbackResumeOnCall();
454 		}
455 	}, lifetime());
456 
__anon022521c50c02null457 	_saveMsgUpdater.setCallback([=] { updateImage(); });
458 
459 	_widget->setAttribute(Qt::WA_AcceptTouchEvents);
__anon022521c50d02null460 	_touchTimer.setCallback([=] { handleTouchTimer(); });
461 
__anon022521c50e02null462 	_controlsHideTimer.setCallback([=] { hideControls(); });
463 
__anon022521c50f02null464 	_docDownload->addClickHandler([=] { downloadMedia(); });
__anon022521c51002null465 	_docSaveAs->addClickHandler([=] { saveAs(); });
__anon022521c51102null466 	_docCancel->addClickHandler([=] { saveCancel(); });
467 
__anon022521c51202null468 	_dropdown->setHiddenCallback([this] { dropdownHidden(); });
__anon022521c51302null469 	_dropdownShowTimer.setCallback([=] { showDropdown(); });
470 }
471 
refreshLang()472 void OverlayWidget::refreshLang() {
473 	InvokeQueued(_widget, [=] { updateThemePreviewGeometry(); });
474 }
475 
moveToScreen()476 void OverlayWidget::moveToScreen() {
477 	const auto widgetScreen = [&](auto &&widget) -> QScreen* {
478 		if (auto handle = widget ? widget->windowHandle() : nullptr) {
479 			return handle->screen();
480 		}
481 		return nullptr;
482 	};
483 	const auto applicationWindow = Core::App().activeWindow()
484 		? Core::App().activeWindow()->widget().get()
485 		: nullptr;
486 	const auto activeWindowScreen = widgetScreen(applicationWindow);
487 	const auto myScreen = widgetScreen(_widget);
488 	if (activeWindowScreen && myScreen != activeWindowScreen) {
489 		const auto screenList = QGuiApplication::screens();
490 		DEBUG_LOG(("Viewer Pos: Currently on screen %1, moving to screen %2")
491 			.arg(screenList.indexOf(myScreen))
492 			.arg(screenList.indexOf(activeWindowScreen)));
493 		window()->setScreen(activeWindowScreen);
494 		DEBUG_LOG(("Viewer Pos: New actual screen: %1")
495 			.arg(screenList.indexOf(window()->screen())));
496 	}
497 	updateGeometry();
498 }
499 
updateGeometry()500 void OverlayWidget::updateGeometry() {
501 	if (Platform::IsWayland()) {
502 		return;
503 	}
504 	const auto screen = window()->screen()
505 		? window()->screen()
506 		: QApplication::primaryScreen();
507 	const auto available = screen->geometry();
508 	const auto openglWidget = _opengl
509 		? static_cast<QOpenGLWidget*>(_widget.get())
510 		: nullptr;
511 	const auto useSizeHack = Platform::IsWindows()
512 		&& openglWidget
513 		&& (openglWidget->format().renderableType()
514 			!= QSurfaceFormat::OpenGLES);
515 	const auto use = useSizeHack
516 		? available.marginsAdded({ 0, 0, 0, 1 })
517 		: available;
518 	const auto mask = useSizeHack
519 		? QRegion(QRect(QPoint(), available.size()))
520 		: QRegion();
521 	if ((_widget->geometry() == use)
522 		&& (!useSizeHack || window()->mask() == mask)) {
523 		return;
524 	}
525 	DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4")
526 		.arg(use.x())
527 		.arg(use.y())
528 		.arg(use.width())
529 		.arg(use.height()));
530 	_widget->setGeometry(use);
531 	if (useSizeHack) {
532 		window()->setMask(mask);
533 	}
534 }
535 
updateControlsGeometry()536 void OverlayWidget::updateControlsGeometry() {
537 	auto navSkip = 2 * st::mediaviewControlMargin + st::mediaviewControlSize;
538 	_closeNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, st::mediaviewControlMargin, st::mediaviewControlSize, st::mediaviewControlSize);
539 	_closeNavIcon = style::centerrect(_closeNav, st::mediaviewClose);
540 	_leftNav = QRect(st::mediaviewControlMargin, navSkip, st::mediaviewControlSize, height() - 2 * navSkip);
541 	_leftNavIcon = style::centerrect(_leftNav, st::mediaviewLeft);
542 	_rightNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip);
543 	_rightNavIcon = style::centerrect(_rightNav, st::mediaviewRight);
544 
545 	_saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2);
546 	_photoRadialRect = QRect(QPoint((width() - st::radialSize.width()) / 2, (height() - st::radialSize.height()) / 2), st::radialSize);
547 
548 	updateControls();
549 	resizeContentByScreenSize();
550 	update();
551 }
552 
flipSizeByRotation(QSize size) const553 QSize OverlayWidget::flipSizeByRotation(QSize size) const {
554 	return FlipSizeByRotation(size, _rotation);
555 }
556 
videoShown() const557 bool OverlayWidget::videoShown() const {
558 	return _streamed && !_streamed->instance.info().video.cover.isNull();
559 }
560 
videoSize() const561 QSize OverlayWidget::videoSize() const {
562 	Expects(videoShown());
563 
564 	return flipSizeByRotation(_streamed->instance.info().video.size);
565 }
566 
videoIsGifOrUserpic() const567 bool OverlayWidget::videoIsGifOrUserpic() const {
568 	return _streamed
569 		&& (!_document
570 			|| (_document->isAnimation() && !_document->isVideoMessage()));
571 }
572 
videoFrame() const573 QImage OverlayWidget::videoFrame() const {
574 	Expects(videoShown());
575 
576 	auto request = Streaming::FrameRequest();
577 	//request.radius = (_document && _document->isVideoMessage())
578 	//	? ImageRoundRadius::Ellipse
579 	//	: ImageRoundRadius::None;
580 	return _streamed->instance.player().ready()
581 		? _streamed->instance.frame(request)
582 		: _streamed->instance.info().video.cover;
583 }
584 
videoFrameWithInfo() const585 Streaming::FrameWithInfo OverlayWidget::videoFrameWithInfo() const {
586 	Expects(videoShown());
587 
588 	return _streamed->instance.player().ready()
589 		? _streamed->instance.frameWithInfo()
590 		: Streaming::FrameWithInfo{
591 			.original = _streamed->instance.info().video.cover,
592 			.format = Streaming::FrameFormat::ARGB32,
593 			.index = -2,
594 		};
595 }
596 
currentVideoFrameImage() const597 QImage OverlayWidget::currentVideoFrameImage() const {
598 	return _streamed->instance.player().ready()
599 		? _streamed->instance.player().currentFrameImage()
600 		: _streamed->instance.info().video.cover;
601 }
602 
streamedIndex() const603 int OverlayWidget::streamedIndex() const {
604 	return _streamedCreated;
605 }
606 
documentContentShown() const607 bool OverlayWidget::documentContentShown() const {
608 	return _document && (!_staticContent.isNull() || videoShown());
609 }
610 
documentBubbleShown() const611 bool OverlayWidget::documentBubbleShown() const {
612 	return (!_photo && !_document)
613 		|| (_document
614 			&& !_themePreviewShown
615 			&& !_streamed
616 			&& _staticContent.isNull());
617 }
618 
setStaticContent(QImage image)619 void OverlayWidget::setStaticContent(QImage image) {
620 	constexpr auto kGood = QImage::Format_ARGB32_Premultiplied;
621 	if (!image.isNull()
622 		&& image.format() != kGood
623 		&& image.format() != QImage::Format_RGB32) {
624 		image = std::move(image).convertToFormat(kGood);
625 	}
626 	image.setDevicePixelRatio(cRetinaFactor());
627 	_staticContent = std::move(image);
628 	_staticContentTransparent = IsSemitransparent(_staticContent);
629 }
630 
contentShown() const631 bool OverlayWidget::contentShown() const {
632 	return _photo || documentContentShown();
633 }
634 
opaqueContentShown() const635 bool OverlayWidget::opaqueContentShown() const {
636 	return contentShown()
637 		&& (!_staticContentTransparent
638 			|| !_document
639 			|| (!_document->isVideoMessage() && !_document->sticker()));
640 }
641 
clearStreaming(bool savePosition)642 void OverlayWidget::clearStreaming(bool savePosition) {
643 	if (_streamed && _document && savePosition) {
644 		Media::Player::SaveLastPlaybackPosition(
645 			_document,
646 			_streamed->instance.player().prepareLegacyState());
647 	}
648 	_fullScreenVideo = false;
649 	_streamed = nullptr;
650 }
651 
documentUpdated(DocumentData * doc)652 void OverlayWidget::documentUpdated(DocumentData *doc) {
653 	if (_document && _document == doc) {
654 		if (documentBubbleShown()) {
655 			if ((_document->loading() && _docCancel->isHidden()) || (!_document->loading() && !_docCancel->isHidden())) {
656 				updateControls();
657 			} else if (_document->loading()) {
658 				updateDocSize();
659 				_widget->update(_docRect);
660 			}
661 		} else if (_streamed) {
662 			const auto ready = _documentMedia->loaded()
663 				? _document->size
664 				: _document->loading()
665 				? std::clamp(_document->loadOffset(), 0, _document->size)
666 				: 0;
667 			_streamed->controls.setLoadingProgress(ready, _document->size);
668 		}
669 	}
670 }
671 
changingMsgId(not_null<HistoryItem * > row,MsgId oldId)672 void OverlayWidget::changingMsgId(not_null<HistoryItem*> row, MsgId oldId) {
673 	if (row == _message) {
674 		refreshMediaViewer();
675 	}
676 }
677 
updateDocSize()678 void OverlayWidget::updateDocSize() {
679 	if (!_document || !documentBubbleShown()) {
680 		return;
681 	}
682 
683 	const auto size = _document->size;
684 	_docSize = _document->loading()
685 		? Ui::FormatProgressText(_document->loadOffset(), size)
686 		: Ui::FormatSizeText(size);
687 	_docSizeWidth = st::mediaviewFont->width(_docSize);
688 	int32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;
689 	if (_docSizeWidth > maxw) {
690 		_docSize = st::mediaviewFont->elided(_docSize, maxw);
691 		_docSizeWidth = st::mediaviewFont->width(_docSize);
692 	}
693 }
694 
refreshNavVisibility()695 void OverlayWidget::refreshNavVisibility() {
696 	if (_sharedMediaData) {
697 		_leftNavVisible = _index && (*_index > 0);
698 		_rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size());
699 	} else if (_userPhotosData) {
700 		_leftNavVisible = _index && (*_index > 0);
701 		_rightNavVisible = _index && (*_index + 1 < _userPhotosData->size());
702 	} else if (_collageData) {
703 		_leftNavVisible = _index && (*_index > 0);
704 		_rightNavVisible = _index && (*_index + 1 < _collageData->items.size());
705 	} else {
706 		_leftNavVisible = false;
707 		_rightNavVisible = false;
708 	}
709 }
710 
contentCanBeSaved() const711 bool OverlayWidget::contentCanBeSaved() const {
712 	if (_photo) {
713 		return _photo->hasVideo() || _photoMedia->loaded();
714 	} else if (_document) {
715 		return _document->filepath(true).isEmpty() && !_document->loading();
716 	} else {
717 		return false;
718 	}
719 }
720 
checkForSaveLoaded()721 void OverlayWidget::checkForSaveLoaded() {
722 	if (_savePhotoVideoWhenLoaded == SavePhotoVideo::None) {
723 		return;
724 	} else if (!_photo
725 		|| !_photo->hasVideo()
726 		|| _photoMedia->videoContent().isEmpty()) {
727 		return;
728 	} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::QuickSave) {
729 		_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
730 		downloadMedia();
731 	} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::SaveAs) {
732 		_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
733 		saveAs();
734 	} else {
735 		Unexpected("SavePhotoVideo in OverlayWidget::checkForSaveLoaded.");
736 	}
737 }
738 
updateControls()739 void OverlayWidget::updateControls() {
740 	if (_document && documentBubbleShown()) {
741 		_docRect = QRect(
742 			(width() - st::mediaviewFileSize.width()) / 2,
743 			(height() - st::mediaviewFileSize.height()) / 2,
744 			st::mediaviewFileSize.width(),
745 			st::mediaviewFileSize.height());
746 		_docIconRect = QRect(
747 			_docRect.x() + st::mediaviewFilePadding,
748 			_docRect.y() + st::mediaviewFilePadding,
749 			st::mediaviewFileIconSize,
750 			st::mediaviewFileIconSize);
751 		if (_document->loading()) {
752 			_docDownload->hide();
753 			_docSaveAs->hide();
754 			_docCancel->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
755 			_docCancel->show();
756 		} else {
757 			if (_documentMedia->loaded(true)) {
758 				_docDownload->hide();
759 				_docSaveAs->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
760 				_docSaveAs->show();
761 				_docCancel->hide();
762 			} else {
763 				_docDownload->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
764 				_docDownload->show();
765 				_docSaveAs->moveToLeft(_docRect.x() + 2.5 * st::mediaviewFilePadding + st::mediaviewFileIconSize + _docDownload->width(), _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
766 				_docSaveAs->show();
767 				_docCancel->hide();
768 			}
769 		}
770 		updateDocSize();
771 	} else {
772 		_docIconRect = QRect(
773 			(width() - st::mediaviewFileIconSize) / 2,
774 			(height() - st::mediaviewFileIconSize) / 2,
775 			st::mediaviewFileIconSize,
776 			st::mediaviewFileIconSize);
777 		_docDownload->hide();
778 		_docSaveAs->hide();
779 		_docCancel->hide();
780 	}
781 	radialStart();
782 
783 	updateThemePreviewGeometry();
784 
785 	_saveVisible = contentCanBeSaved();
786 	_rotateVisible = !_themePreviewShown;
787 	const auto navRect = [&](int i) {
788 		return QRect(width() - st::mediaviewIconSize.width() * i,
789 			height() - st::mediaviewIconSize.height(),
790 			st::mediaviewIconSize.width(),
791 			st::mediaviewIconSize.height());
792 	};
793 	_saveNav = navRect(_rotateVisible ? 3 : 2);
794 	_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
795 	_rotateNav = navRect(2);
796 	_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);
797 	_moreNav = navRect(1);
798 	_moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore);
799 
800 	const auto dNow = QDateTime::currentDateTime();
801 	const auto d = [&] {
802 		if (_message) {
803 			return ItemDateTime(_message);
804 		} else if (_photo) {
805 			return base::unixtime::parse(_photo->date);
806 		} else if (_document) {
807 			return base::unixtime::parse(_document->date);
808 		}
809 		return dNow;
810 	}();
811 	_dateText = Ui::FormatDateTime(d, cDateFormat(), cTimeFormat());
812 	if (!_fromName.isEmpty()) {
813 		_fromNameLabel.setText(st::mediaviewTextStyle, _fromName, Ui::NameTextOptions());
814 		_nameNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height);
815 		_dateNav = QRect(st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height);
816 	} else {
817 		_nameNav = QRect();
818 		_dateNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height);
819 	}
820 	updateHeader();
821 	refreshNavVisibility();
822 	resizeCenteredControls();
823 
824 	updateOver(_widget->mapFromGlobal(QCursor::pos()));
825 	update();
826 }
827 
resizeCenteredControls()828 void OverlayWidget::resizeCenteredControls() {
829 	const auto bottomSkip = std::max(
830 		_dateNav.left() + _dateNav.width(),
831 		_headerNav.left() + _headerNav.width())
832 		+ st::mediaviewCaptionMargin.width();
833 	_groupThumbsAvailableWidth = std::max(
834 		width() - 2 * bottomSkip,
835 		st::msgMinWidth
836 		+ st::mediaviewCaptionPadding.left()
837 		+ st::mediaviewCaptionPadding.right());
838 	_groupThumbsLeft = (width() - _groupThumbsAvailableWidth) / 2;
839 	refreshGroupThumbs();
840 	_groupThumbsTop = _groupThumbs ? (height() - _groupThumbs->height()) : 0;
841 
842 	refreshClipControllerGeometry();
843 	refreshCaptionGeometry();
844 }
845 
refreshCaptionGeometry()846 void OverlayWidget::refreshCaptionGeometry() {
847 	if (_caption.isEmpty()) {
848 		_captionRect = QRect();
849 		return;
850 	}
851 
852 	if (_groupThumbs && _groupThumbs->hiding()) {
853 		_groupThumbs = nullptr;
854 		_groupThumbsRect = QRect();
855 	}
856 	const auto captionBottom = (_streamed && !videoIsGifOrUserpic())
857 		? (_streamed->controls.y() - st::mediaviewCaptionMargin.height())
858 		: _groupThumbs
859 		? _groupThumbsTop
860 		: height() - st::mediaviewCaptionMargin.height();
861 	const auto captionWidth = std::min(
862 		_groupThumbsAvailableWidth
863 		- st::mediaviewCaptionPadding.left()
864 		- st::mediaviewCaptionPadding.right(),
865 		_caption.maxWidth());
866 	const auto captionHeight = std::min(
867 		_caption.countHeight(captionWidth),
868 		height() / 4
869 		- st::mediaviewCaptionPadding.top()
870 		- st::mediaviewCaptionPadding.bottom()
871 		- 2 * st::mediaviewCaptionMargin.height());
872 	_captionRect = QRect(
873 		(width() - captionWidth) / 2,
874 		captionBottom
875 		- captionHeight
876 		- st::mediaviewCaptionPadding.bottom(),
877 		captionWidth,
878 		captionHeight);
879 }
880 
fillContextMenuActions(const MenuCallback & addAction)881 void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
882 	if (_document && _document->loading()) {
883 		addAction(tr::lng_cancel(tr::now), [=] { saveCancel(); });
884 	}
885 	if (_message && _message->isRegular()) {
886 		addAction(tr::lng_context_to_msg(tr::now), [=] { toMessage(); });
887 	}
888 	if (_document && !_document->filepath(true).isEmpty()) {
889 		const auto text =  Platform::IsMac()
890 			? tr::lng_context_show_in_finder(tr::now)
891 			: tr::lng_context_show_in_folder(tr::now);
892 		addAction(text, [=] { showInFolder(); });
893 	}
894 	if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) {
895 		addAction(tr::lng_mediaview_copy(tr::now), [=] { copyMedia(); });
896 	}
897 	if ((_photo && _photo->hasAttachedStickers())
898 		|| (_document && _document->hasAttachedStickers())) {
899 		addAction(
900 			tr::lng_context_attached_stickers(tr::now),
901 			[=] { showAttachedStickers(); });
902 	}
903 	if (_message && _message->allowsForward()) {
904 		addAction(tr::lng_mediaview_forward(tr::now), [=] { forwardMedia(); });
905 	}
906 	const auto canDelete = [&] {
907 		if (_message && _message->canDelete()) {
908 			return true;
909 		} else if (!_message
910 			&& _photo
911 			&& _user
912 			&& _user == _user->session().user()) {
913 			return _userPhotosData && _fullIndex && _fullCount;
914 		} else if (_photo && _photo->peer && _photo->peer->userpicPhotoId() == _photo->id) {
915 			if (auto chat = _photo->peer->asChat()) {
916 				return chat->canEditInformation();
917 			} else if (auto channel = _photo->peer->asChannel()) {
918 				return channel->canEditInformation();
919 			}
920 		}
921 		return false;
922 	}();
923 	if (canDelete) {
924 		addAction(tr::lng_mediaview_delete(tr::now), [=] { deleteMedia(); });
925 	}
926 	addAction(tr::lng_mediaview_save_as(tr::now), [=] { saveAs(); });
927 
928 	if (const auto overviewType = computeOverviewType()) {
929 		const auto text = _document
930 			? tr::lng_mediaview_files_all(tr::now)
931 			: tr::lng_mediaview_photos_all(tr::now);
932 		addAction(text, [=] { showMediaOverview(); });
933 	}
934 }
935 
computeOverviewType() const936 auto OverlayWidget::computeOverviewType() const
937 -> std::optional<SharedMediaType> {
938 	if (const auto mediaType = sharedMediaType()) {
939 		if (const auto overviewType = SharedMediaOverviewType(*mediaType)) {
940 			return overviewType;
941 		} else if (mediaType == SharedMediaType::PhotoVideo) {
942 			if (_photo) {
943 				return SharedMediaOverviewType(SharedMediaType::Photo);
944 			} else if (_document) {
945 				return SharedMediaOverviewType(SharedMediaType::Video);
946 			}
947 		}
948 	}
949 	return std::nullopt;
950 }
951 
stateAnimationCallback(crl::time now)952 bool OverlayWidget::stateAnimationCallback(crl::time now) {
953 	if (anim::Disabled()) {
954 		now += st::mediaviewShowDuration + st::mediaviewHideDuration;
955 	}
956 	for (auto i = begin(_animations); i != end(_animations);) {
957 		const auto [state, started] = *i;
958 		updateOverRect(state);
959 		const auto dt = float64(now - started) / st::mediaviewFadeDuration;
960 		if (dt >= 1) {
961 			_animationOpacities.erase(state);
962 			i = _animations.erase(i);
963 		} else {
964 			_animationOpacities[state].update(dt, anim::linear);
965 			++i;
966 		}
967 	}
968 	return !_animations.empty() || updateControlsAnimation(now);
969 }
970 
updateControlsAnimation(crl::time now)971 bool OverlayWidget::updateControlsAnimation(crl::time now) {
972 	if (_controlsState != ControlsShowing
973 		&& _controlsState != ControlsHiding) {
974 		return false;
975 	}
976 	const auto duration = (_controlsState == ControlsShowing)
977 		? st::mediaviewShowDuration
978 		: st::mediaviewHideDuration;
979 	const auto dt = float64(now - _controlsAnimStarted)
980 		/ duration;
981 	if (dt >= 1) {
982 		_controlsOpacity.finish();
983 		_controlsState = (_controlsState == ControlsShowing)
984 			? ControlsShown
985 			: ControlsHidden;
986 		updateCursor();
987 	} else {
988 		_controlsOpacity.update(dt, anim::linear);
989 	}
990 	const auto toUpdate = QRegion()
991 		+ (_over == OverLeftNav ? _leftNav : _leftNavIcon)
992 		+ (_over == OverRightNav ? _rightNav : _rightNavIcon)
993 		+ (_over == OverClose ? _closeNav : _closeNavIcon)
994 		+ _saveNavIcon
995 		+ _rotateNavIcon
996 		+ _moreNavIcon
997 		+ _headerNav
998 		+ _nameNav
999 		+ _dateNav
1000 		+ _captionRect.marginsAdded(st::mediaviewCaptionPadding)
1001 		+ _groupThumbsRect;
1002 	update(toUpdate);
1003 	return (dt < 1);
1004 }
1005 
waitingAnimationCallback()1006 void OverlayWidget::waitingAnimationCallback() {
1007 	if (!anim::Disabled()) {
1008 		update(radialRect());
1009 	}
1010 }
1011 
updateCursor()1012 void OverlayWidget::updateCursor() {
1013 	setCursor(_controlsState == ControlsHidden
1014 		? Qt::BlankCursor
1015 		: (_over == OverNone ? style::cur_default : style::cur_pointer));
1016 }
1017 
finalContentRotation() const1018 int OverlayWidget::finalContentRotation() const {
1019 	return _streamed
1020 		? ((_rotation + (_streamed
1021 			? _streamed->instance.info().video.rotation
1022 			: 0)) % 360)
1023 		: _rotation;
1024 }
1025 
finalContentRect() const1026 QRect OverlayWidget::finalContentRect() const {
1027 	return { _x, _y, _w, _h };
1028 }
1029 
contentGeometry() const1030 OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const {
1031 	const auto toRotation = qreal(finalContentRotation());
1032 	const auto toRectRotated = QRectF(finalContentRect());
1033 	const auto toRectCenter = toRectRotated.center();
1034 	const auto toRect = ((int(toRotation) % 180) == 90)
1035 		? QRectF(
1036 			toRectCenter.x() - toRectRotated.height() / 2.,
1037 			toRectCenter.y() - toRectRotated.width() / 2.,
1038 			toRectRotated.height(),
1039 			toRectRotated.width())
1040 		: toRectRotated;
1041 	if (!_geometryAnimation.animating()) {
1042 		return { toRect, toRotation };
1043 	}
1044 	const auto fromRect = _oldGeometry.rect;
1045 	const auto fromRotation = _oldGeometry.rotation;
1046 	const auto progress = _geometryAnimation.value(1.);
1047 	const auto rotationDelta = (toRotation - fromRotation);
1048 	const auto useRotationDelta = (rotationDelta > 180.)
1049 		? (rotationDelta - 360.)
1050 		: (rotationDelta <= -180.)
1051 		? (rotationDelta + 360.)
1052 		: rotationDelta;
1053 	const auto rotation = fromRotation + useRotationDelta * progress;
1054 	const auto useRotation = (rotation > 360.)
1055 		? (rotation - 360.)
1056 		: (rotation < 0.)
1057 		? (rotation + 360.)
1058 		: rotation;
1059 	const auto useRect = QRectF(
1060 		fromRect.x() + (toRect.x() - fromRect.x()) * progress,
1061 		fromRect.y() + (toRect.y() - fromRect.y()) * progress,
1062 		fromRect.width() + (toRect.width() - fromRect.width()) * progress,
1063 		fromRect.height() + (toRect.height() - fromRect.height()) * progress
1064 	);
1065 	return { useRect, useRotation };
1066 }
1067 
updateContentRect()1068 void OverlayWidget::updateContentRect() {
1069 	if (_opengl) {
1070 		update();
1071 	} else {
1072 		update(finalContentRect());
1073 	}
1074 }
1075 
contentSizeChanged()1076 void OverlayWidget::contentSizeChanged() {
1077 	_width = _w;
1078 	_height = _h;
1079 	resizeContentByScreenSize();
1080 }
1081 
resizeContentByScreenSize()1082 void OverlayWidget::resizeContentByScreenSize() {
1083 	const auto bottom = (!_streamed || videoIsGifOrUserpic())
1084 		? height()
1085 		: (_streamed->controls.y()
1086 			- st::mediaviewCaptionPadding.bottom()
1087 			- st::mediaviewCaptionMargin.height());
1088 	const auto skipHeight = (height() - bottom);
1089 	const auto availableWidth = width();
1090 	const auto availableHeight = height() - 2 * skipHeight;
1091 	const auto countZoomFor = [&](int outerw, int outerh) {
1092 		auto result = float64(outerw) / _width;
1093 		if (_height * result > outerh) {
1094 			result = float64(outerh) / _height;
1095 		}
1096 		if (result >= 1.) {
1097 			result -= 1.;
1098 		} else {
1099 			result = 1. - (1. / result);
1100 		}
1101 		return result;
1102 	};
1103 	if (_width > 0 && _height > 0) {
1104 		_zoomToDefault = countZoomFor(availableWidth, availableHeight);
1105 		_zoomToScreen = countZoomFor(width(), height());
1106 	} else {
1107 		_zoomToDefault = _zoomToScreen = 0;
1108 	}
1109 	const auto usew = _fullScreenVideo ? width() : availableWidth;
1110 	const auto useh = _fullScreenVideo ? height() : availableHeight;
1111 	if ((_width > usew) || (_height > useh) || _fullScreenVideo) {
1112 		const auto use = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
1113 		_zoom = kZoomToScreenLevel;
1114 		if (use >= 0) {
1115 			_w = qRound(_width * (use + 1));
1116 			_h = qRound(_height * (use + 1));
1117 		} else {
1118 			_w = qRound(_width / (-use + 1));
1119 			_h = qRound(_height / (-use + 1));
1120 		}
1121 	} else {
1122 		_zoom = 0;
1123 		_w = _width;
1124 		_h = _height;
1125 	}
1126 	_x = (width() - _w) / 2;
1127 	_y = (height() - _h) / 2;
1128 	_geometryAnimation.stop();
1129 }
1130 
radialProgress() const1131 float64 OverlayWidget::radialProgress() const {
1132 	if (_document) {
1133 		return _documentMedia->progress();
1134 	} else if (_photo) {
1135 		return _photoMedia->progress();
1136 	}
1137 	return 1.;
1138 }
1139 
radialLoading() const1140 bool OverlayWidget::radialLoading() const {
1141 	if (_streamed) {
1142 		return false;
1143 	} else if (_document) {
1144 		return _document->loading();
1145 	} else if (_photo) {
1146 		return _photo->displayLoading();
1147 	}
1148 	return false;
1149 }
1150 
radialRect() const1151 QRect OverlayWidget::radialRect() const {
1152 	if (_photo) {
1153 		return _photoRadialRect;
1154 	} else if (_document) {
1155 		return QRect(
1156 			QPoint(
1157 				_docIconRect.x() + ((_docIconRect.width() - st::radialSize.width()) / 2),
1158 				_docIconRect.y() + ((_docIconRect.height() - st::radialSize.height()) / 2)),
1159 			st::radialSize);
1160 	}
1161 	return QRect();
1162 }
1163 
radialStart()1164 void OverlayWidget::radialStart() {
1165 	if (radialLoading() && !_radial.animating()) {
1166 		_radial.start(radialProgress());
1167 		if (auto shift = radialTimeShift()) {
1168 			_radial.update(radialProgress(), !radialLoading(), crl::now() + shift);
1169 		}
1170 	}
1171 }
1172 
radialTimeShift() const1173 crl::time OverlayWidget::radialTimeShift() const {
1174 	return _photo ? st::radialDuration : 0;
1175 }
1176 
radialAnimationCallback(crl::time now)1177 bool OverlayWidget::radialAnimationCallback(crl::time now) {
1178 	if ((!_document && !_photo) || _streamed) {
1179 		return false;
1180 	}
1181 	const auto wasAnimating = _radial.animating();
1182 	const auto updated = _radial.update(
1183 		radialProgress(),
1184 		!radialLoading(),
1185 		now + radialTimeShift());
1186 	if ((wasAnimating || _radial.animating())
1187 		&& (!anim::Disabled() || updated)) {
1188 		update(radialRect());
1189 	}
1190 	const auto ready = _document && _documentMedia->loaded();
1191 	const auto streamVideo = ready && _documentMedia->canBePlayed();
1192 	const auto tryOpenImage = ready
1193 		&& (_document->size < Images::kReadBytesLimit);
1194 	if (ready && ((tryOpenImage && !_radial.animating()) || streamVideo)) {
1195 		_streamingStartPaused = false;
1196 		if (streamVideo) {
1197 			redisplayContent();
1198 		} else {
1199 			auto &location = _document->location(true);
1200 			if (location.accessEnable()) {
1201 				if (_document->isTheme()
1202 					|| QImageReader(location.name()).canRead()) {
1203 					redisplayContent();
1204 				}
1205 				location.accessDisable();
1206 			}
1207 		}
1208 	}
1209 	return true;
1210 }
1211 
zoomIn()1212 void OverlayWidget::zoomIn() {
1213 	auto newZoom = _zoom;
1214 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
1215 	if (newZoom == kZoomToScreenLevel) {
1216 		if (qCeil(full) <= kMaxZoomLevel) {
1217 			newZoom = qCeil(full);
1218 		}
1219 	} else {
1220 		if (newZoom < full && (newZoom + 1 > full || (full > kMaxZoomLevel && newZoom == kMaxZoomLevel))) {
1221 			newZoom = kZoomToScreenLevel;
1222 		} else if (newZoom < kMaxZoomLevel) {
1223 			++newZoom;
1224 		}
1225 	}
1226 	zoomUpdate(newZoom);
1227 }
1228 
zoomOut()1229 void OverlayWidget::zoomOut() {
1230 	auto newZoom = _zoom;
1231 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
1232 	if (newZoom == kZoomToScreenLevel) {
1233 		if (qFloor(full) >= -kMaxZoomLevel) {
1234 			newZoom = qFloor(full);
1235 		}
1236 	} else {
1237 		if (newZoom > full && (newZoom - 1 < full || (full < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) {
1238 			newZoom = kZoomToScreenLevel;
1239 		} else if (newZoom > -kMaxZoomLevel) {
1240 			--newZoom;
1241 		}
1242 	}
1243 	zoomUpdate(newZoom);
1244 }
1245 
zoomReset()1246 void OverlayWidget::zoomReset() {
1247 	auto newZoom = _zoom;
1248 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
1249 	if (_zoom == 0) {
1250 		if (qFloor(full) == qCeil(full) && qRound(full) >= -kMaxZoomLevel && qRound(full) <= kMaxZoomLevel) {
1251 			newZoom = qRound(full);
1252 		} else {
1253 			newZoom = kZoomToScreenLevel;
1254 		}
1255 	} else {
1256 		newZoom = 0;
1257 	}
1258 	_x = -_width / 2;
1259 	_y = -_height / 2;
1260 	float64 z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
1261 	if (z >= 0) {
1262 		_x = qRound(_x * (z + 1));
1263 		_y = qRound(_y * (z + 1));
1264 	} else {
1265 		_x = qRound(_x / (-z + 1));
1266 		_y = qRound(_y / (-z + 1));
1267 	}
1268 	_x += width() / 2;
1269 	_y += height() / 2;
1270 	update();
1271 	zoomUpdate(newZoom);
1272 }
1273 
zoomUpdate(int32 & newZoom)1274 void OverlayWidget::zoomUpdate(int32 &newZoom) {
1275 	if (newZoom != kZoomToScreenLevel) {
1276 		while ((newZoom < 0 && (-newZoom + 1) > _w) || (-newZoom + 1) > _h) {
1277 			++newZoom;
1278 		}
1279 	}
1280 	setZoomLevel(newZoom);
1281 }
1282 
clearSession()1283 void OverlayWidget::clearSession() {
1284 	if (!isHidden()) {
1285 		hide();
1286 	}
1287 	_sessionLifetime.destroy();
1288 	if (!_animations.empty()) {
1289 		_animations.clear();
1290 		_stateAnimation.stop();
1291 	}
1292 	if (!_animationOpacities.empty()) {
1293 		_animationOpacities.clear();
1294 	}
1295 	clearStreaming();
1296 	setContext(v::null);
1297 	_from = nullptr;
1298 	_fromName = QString();
1299 	assignMediaPointer(nullptr);
1300 	_fullScreenVideo = false;
1301 	_caption.clear();
1302 	_sharedMedia = nullptr;
1303 	_userPhotos = nullptr;
1304 	_collage = nullptr;
1305 	_session = nullptr;
1306 }
1307 
~OverlayWidget()1308 OverlayWidget::~OverlayWidget() {
1309 	clearSession();
1310 }
1311 
assignMediaPointer(DocumentData * document)1312 void OverlayWidget::assignMediaPointer(DocumentData *document) {
1313 	_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
1314 	_photo = nullptr;
1315 	_photoMedia = nullptr;
1316 	if (_document != document) {
1317 		if ((_document = document)) {
1318 			_documentMedia = _document->createMediaView();
1319 			_documentMedia->goodThumbnailWanted();
1320 			_documentMedia->thumbnailWanted(fileOrigin());
1321 		} else {
1322 			_documentMedia = nullptr;
1323 		}
1324 	}
1325 }
1326 
assignMediaPointer(not_null<PhotoData * > photo)1327 void OverlayWidget::assignMediaPointer(not_null<PhotoData*> photo) {
1328 	_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
1329 	_document = nullptr;
1330 	_documentMedia = nullptr;
1331 	if (_photo != photo) {
1332 		_photo = photo;
1333 		_photoMedia = _photo->createMediaView();
1334 		_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());
1335 		if (!_photo->hasVideo() || _photo->videoPlaybackFailed()) {
1336 			_photo->load(fileOrigin(), LoadFromCloudOrLocal, true);
1337 		}
1338 	}
1339 }
1340 
clickHandlerActiveChanged(const ClickHandlerPtr & p,bool active)1341 void OverlayWidget::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
1342 	setCursor((active || ClickHandler::getPressed()) ? style::cur_pointer : style::cur_default);
1343 	update(QRegion(_saveMsg) + _captionRect);
1344 }
1345 
clickHandlerPressedChanged(const ClickHandlerPtr & p,bool pressed)1346 void OverlayWidget::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
1347 	setCursor((pressed || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
1348 	update(QRegion(_saveMsg) + _captionRect);
1349 }
1350 
lifetime()1351 rpl::lifetime &OverlayWidget::lifetime() {
1352 	return _surface->lifetime();
1353 }
1354 
showSaveMsgFile()1355 void OverlayWidget::showSaveMsgFile() {
1356 	File::ShowInFolder(_saveMsgFilename);
1357 }
1358 
close()1359 void OverlayWidget::close() {
1360 	Core::App().hideMediaView();
1361 }
1362 
activateControls()1363 void OverlayWidget::activateControls() {
1364 	if (!_menu && !_mousePressed) {
1365 		_controlsHideTimer.callOnce(st::mediaviewWaitHide);
1366 	}
1367 	if (_fullScreenVideo) {
1368 		if (_streamed) {
1369 			_streamed->controls.showAnimated();
1370 		}
1371 	}
1372 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) {
1373 		_controlsState = ControlsShowing;
1374 		_controlsAnimStarted = crl::now();
1375 		_controlsOpacity.start(1);
1376 		if (!_stateAnimation.animating()) {
1377 			_stateAnimation.start();
1378 		}
1379 	}
1380 }
1381 
hideControls(bool force)1382 void OverlayWidget::hideControls(bool force) {
1383 	if (!force) {
1384 		if (!_dropdown->isHidden()
1385 			|| (_streamed && _streamed->controls.hasMenu())
1386 			|| _menu
1387 			|| _mousePressed
1388 			|| (_fullScreenVideo
1389 				&& !videoIsGifOrUserpic()
1390 				&& _streamed->controls.geometry().contains(_lastMouseMovePos))) {
1391 			return;
1392 		}
1393 	}
1394 	if (_fullScreenVideo) {
1395 		_streamed->controls.hideAnimated();
1396 	}
1397 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return;
1398 
1399 	_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());
1400 	_controlsState = ControlsHiding;
1401 	_controlsAnimStarted = crl::now();
1402 	_controlsOpacity.start(0);
1403 	if (!_stateAnimation.animating()) {
1404 		_stateAnimation.start();
1405 	}
1406 }
1407 
dropdownHidden()1408 void OverlayWidget::dropdownHidden() {
1409 	setFocus();
1410 	_ignoringDropdown = true;
1411 	_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());
1412 	updateOver(_lastMouseMovePos);
1413 	_ignoringDropdown = false;
1414 	if (!_controlsHideTimer.isActive()) {
1415 		hideControls(true);
1416 	}
1417 }
1418 
handleScreenChanged(QScreen * screen)1419 void OverlayWidget::handleScreenChanged(QScreen *screen) {
1420 	subscribeToScreenGeometry();
1421 	if (isHidden()) {
1422 		return;
1423 	}
1424 
1425 	const auto screenList = QGuiApplication::screens();
1426 	DEBUG_LOG(("Viewer Pos: Screen changed to: %1")
1427 		.arg(screenList.indexOf(screen)));
1428 
1429 	moveToScreen();
1430 }
1431 
subscribeToScreenGeometry()1432 void OverlayWidget::subscribeToScreenGeometry() {
1433 	_screenGeometryLifetime.destroy();
1434 	const auto screen = window()->screen();
1435 	if (!screen) {
1436 		return;
1437 	}
1438 	base::qt_signal_producer(
1439 		screen,
1440 		&QScreen::geometryChanged
1441 	) | rpl::start_with_next([=] {
1442 		updateGeometry();
1443 	}, _screenGeometryLifetime);
1444 }
1445 
toMessage()1446 void OverlayWidget::toMessage() {
1447 	if (const auto item = _message) {
1448 		close();
1449 		if (const auto window = findWindow()) {
1450 			window->showPeerHistoryAtItem(item);
1451 		}
1452 	}
1453 }
1454 
notifyFileDialogShown(bool shown)1455 void OverlayWidget::notifyFileDialogShown(bool shown) {
1456 	if (shown && isHidden()) {
1457 		return;
1458 	}
1459 	if (shown) {
1460 		Ui::Platform::BringToBack(_widget);
1461 	} else {
1462 		Ui::Platform::ShowOverAll(_widget);
1463 	}
1464 }
1465 
saveAs()1466 void OverlayWidget::saveAs() {
1467 	QString file;
1468 	if (_document) {
1469 		const auto &location = _document->location(true);
1470 		const auto bytes = _documentMedia->bytes();
1471 		if (!bytes.isEmpty() || location.accessEnable()) {
1472 			QFileInfo alreadyInfo(location.name());
1473 			QDir alreadyDir(alreadyInfo.dir());
1474 			QString name = alreadyInfo.fileName(), filter;
1475 			const auto mimeType = Core::MimeTypeForName(_document->mimeString());
1476 			QStringList p = mimeType.globPatterns();
1477 			QString pattern = p.isEmpty() ? QString() : p.front();
1478 			if (name.isEmpty()) {
1479 				name = pattern.isEmpty() ? qsl(".unknown") : pattern.replace('*', QString());
1480 			}
1481 
1482 			if (pattern.isEmpty()) {
1483 				filter = QString();
1484 			} else {
1485 				filter = mimeType.filterString() + qsl(";;") + FileDialog::AllFilesFilter();
1486 			}
1487 
1488 			file = FileNameForSave(
1489 				_session,
1490 				tr::lng_save_file(tr::now),
1491 				filter,
1492 				qsl("doc"),
1493 				name,
1494 				true,
1495 				alreadyDir);
1496 			if (!file.isEmpty() && file != location.name()) {
1497 				if (bytes.isEmpty()) {
1498 					QFile(file).remove();
1499 					QFile(location.name()).copy(file);
1500 				} else {
1501 					QFile f(file);
1502 					f.open(QIODevice::WriteOnly);
1503 					f.write(bytes);
1504 				}
1505 			}
1506 
1507 			if (bytes.isEmpty()) {
1508 				location.accessDisable();
1509 			}
1510 		} else {
1511 			DocumentSaveClickHandler::Save(
1512 				fileOrigin(),
1513 				_document,
1514 				DocumentSaveClickHandler::Mode::ToNewFile);
1515 			updateControls();
1516 			updateOver(_lastMouseMovePos);
1517 		}
1518 	} else if (_photo && _photo->hasVideo()) {
1519 		if (const auto bytes = _photoMedia->videoContent(); !bytes.isEmpty()) {
1520 			const auto photo = _photo;
1521 			auto filter = qsl("Video Files (*.mp4);;") + FileDialog::AllFilesFilter();
1522 			FileDialog::GetWritePath(
1523 				_widget.get(),
1524 				tr::lng_save_video(tr::now),
1525 				filter,
1526 				filedialogDefaultName(
1527 					qsl("photo"),
1528 					qsl(".mp4"),
1529 					QString(),
1530 					false,
1531 					_photo->date),
1532 				crl::guard(_widget, [=](const QString &result) {
1533 					QFile f(result);
1534 					if (!result.isEmpty()
1535 						&& _photo == photo
1536 						&& f.open(QIODevice::WriteOnly)) {
1537 						f.write(bytes);
1538 					}
1539 				}));
1540 		} else {
1541 			_photo->loadVideo(fileOrigin());
1542 			_savePhotoVideoWhenLoaded = SavePhotoVideo::SaveAs;
1543 		}
1544 	} else {
1545 		if (!_photo || !_photoMedia->loaded()) {
1546 			return;
1547 		}
1548 
1549 		const auto image = _photoMedia->image(Data::PhotoSize::Large)->original();
1550 		const auto photo = _photo;
1551 		auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter();
1552 		FileDialog::GetWritePath(
1553 			_widget.get(),
1554 			tr::lng_save_photo(tr::now),
1555 			filter,
1556 			filedialogDefaultName(
1557 				qsl("photo"),
1558 				qsl(".jpg"),
1559 				QString(),
1560 				false,
1561 				_photo->date),
1562 			crl::guard(_widget, [=](const QString &result) {
1563 				if (!result.isEmpty() && _photo == photo) {
1564 					image.save(result, "JPG");
1565 				}
1566 			}));
1567 	}
1568 	activate();
1569 }
1570 
handleDocumentClick()1571 void OverlayWidget::handleDocumentClick() {
1572 	if (_document->loading()) {
1573 		saveCancel();
1574 	} else {
1575 		Data::ResolveDocument(findWindow(), _document, _message);
1576 		if (_document->loading() && !_radial.animating()) {
1577 			_radial.start(_documentMedia->progress());
1578 		}
1579 	}
1580 }
1581 
downloadMedia()1582 void OverlayWidget::downloadMedia() {
1583 	if (!_photo && !_document) {
1584 		return;
1585 	}
1586 	if (Core::App().settings().askDownloadPath()) {
1587 		return saveAs();
1588 	}
1589 
1590 	QString path;
1591 	const auto session = _photo ? &_photo->session() : &_document->session();
1592 	if (Core::App().settings().downloadPath().isEmpty()) {
1593 		path = File::DefaultDownloadPath(session);
1594 	} else if (Core::App().settings().downloadPath() == qsl("tmp")) {
1595 		path = session->local().tempDirectory();
1596 	} else {
1597 		path = Core::App().settings().downloadPath();
1598 	}
1599 	QString toName;
1600 	if (_document) {
1601 		const auto &location = _document->location(true);
1602 		if (location.accessEnable()) {
1603 			if (!QDir().exists(path)) QDir().mkpath(path);
1604 			toName = filedialogNextFilename(
1605 				_document->filename(),
1606 				location.name(),
1607 				path);
1608 			if (!toName.isEmpty() && toName != location.name()) {
1609 				QFile(toName).remove();
1610 				if (!QFile(location.name()).copy(toName)) {
1611 					toName = QString();
1612 				}
1613 			}
1614 			location.accessDisable();
1615 		} else {
1616 			if (_document->filepath(true).isEmpty()
1617 				&& !_document->loading()) {
1618 				DocumentSaveClickHandler::Save(
1619 					fileOrigin(),
1620 					_document,
1621 					DocumentSaveClickHandler::Mode::ToFile);
1622 				updateControls();
1623 			} else {
1624 				_saveVisible = contentCanBeSaved();
1625 				update(_saveNav);
1626 			}
1627 			updateOver(_lastMouseMovePos);
1628 		}
1629 	} else if (_photo && _photo->hasVideo()) {
1630 		if (const auto bytes = _photoMedia->videoContent(); !bytes.isEmpty()) {
1631 			if (!QDir().exists(path)) {
1632 				QDir().mkpath(path);
1633 			}
1634 			toName = filedialogDefaultName(qsl("photo"), qsl(".mp4"), path);
1635 			QFile f(toName);
1636 			if (!f.open(QIODevice::WriteOnly)
1637 				|| f.write(bytes) != bytes.size()) {
1638 				toName = QString();
1639 			}
1640 		} else {
1641 			_photo->loadVideo(fileOrigin());
1642 			_savePhotoVideoWhenLoaded = SavePhotoVideo::QuickSave;
1643 		}
1644 	} else {
1645 		if (!_photo || !_photoMedia->loaded()) {
1646 			_saveVisible = contentCanBeSaved();
1647 			update(_saveNav);
1648 		} else {
1649 			const auto image = _photoMedia->image(
1650 				Data::PhotoSize::Large)->original();
1651 
1652 			if (!QDir().exists(path)) {
1653 				QDir().mkpath(path);
1654 			}
1655 			toName = filedialogDefaultName(qsl("photo"), qsl(".jpg"), path);
1656 			if (!image.save(toName, "JPG")) {
1657 				toName = QString();
1658 			}
1659 		}
1660 	}
1661 	if (!toName.isEmpty()) {
1662 		_saveMsgFilename = toName;
1663 		_saveMsgStarted = crl::now();
1664 		_saveMsgOpacity.start(1);
1665 		updateImage();
1666 	}
1667 }
1668 
saveCancel()1669 void OverlayWidget::saveCancel() {
1670 	if (_document && _document->loading()) {
1671 		_document->cancel();
1672 		if (_documentMedia->canBePlayed()) {
1673 			redisplayContent();
1674 		}
1675 	}
1676 }
1677 
showInFolder()1678 void OverlayWidget::showInFolder() {
1679 	if (!_document) return;
1680 
1681 	auto filepath = _document->filepath(true);
1682 	if (!filepath.isEmpty()) {
1683 		File::ShowInFolder(filepath);
1684 		close();
1685 	}
1686 }
1687 
forwardMedia()1688 void OverlayWidget::forwardMedia() {
1689 	if (!_session) {
1690 		return;
1691 	}
1692 	const auto &active = _session->windows();
1693 	if (active.empty()) {
1694 		return;
1695 	}
1696 	const auto id = (_message && _message->allowsForward())
1697 		? _message->fullId()
1698 		: FullMsgId();
1699 	if (id) {
1700 		close();
1701 		Window::ShowForwardMessagesBox(active.front(), { 1, id });
1702 	}
1703 }
1704 
deleteMedia()1705 void OverlayWidget::deleteMedia() {
1706 	if (!_session) {
1707 		return;
1708 	}
1709 
1710 	const auto session = _session;
1711 	const auto photo = _photo;
1712 	const auto message = _message;
1713 	const auto deletingPeerPhoto = [&] {
1714 		if (!_message) {
1715 			return true;
1716 		} else if (_photo && _history) {
1717 			if (_history->peer->userpicPhotoId() == _photo->id) {
1718 				return _firstOpenedPeerPhoto;
1719 			}
1720 		}
1721 		return false;
1722 	}();
1723 	close();
1724 
1725 	if (const auto window = findWindow()) {
1726 		if (deletingPeerPhoto) {
1727 			if (photo) {
1728 				window->show(
1729 					Box<Ui::ConfirmBox>(
1730 						tr::lng_delete_photo_sure(tr::now),
1731 						tr::lng_box_delete(tr::now),
1732 						crl::guard(_widget, [=] {
1733 							session->api().peerPhoto().clear(photo);
1734 							Ui::hideLayer();
1735 						})),
1736 					Ui::LayerOption::CloseOther);
1737 			}
1738 		} else if (message) {
1739 			const auto suggestModerateActions = true;
1740 			window->show(
1741 				Box<DeleteMessagesBox>(message, suggestModerateActions),
1742 				Ui::LayerOption::CloseOther);
1743 		}
1744 	}
1745 }
1746 
showMediaOverview()1747 void OverlayWidget::showMediaOverview() {
1748 	if (_menu) {
1749 		_menu->hideMenu(true);
1750 	}
1751 	update();
1752 	if (const auto overviewType = computeOverviewType()) {
1753 		close();
1754 		SharedMediaShowOverview(*overviewType, _history);
1755 	}
1756 }
1757 
copyMedia()1758 void OverlayWidget::copyMedia() {
1759 	_dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow);
1760 	if (_document) {
1761 		QGuiApplication::clipboard()->setImage(transformedShownContent());
1762 	} else if (_photo && _photoMedia->loaded()) {
1763 		const auto image = _photoMedia->image(
1764 			Data::PhotoSize::Large)->original();
1765 		QGuiApplication::clipboard()->setImage(image);
1766 	}
1767 }
1768 
showAttachedStickers()1769 void OverlayWidget::showAttachedStickers() {
1770 	if (!_session) {
1771 		return;
1772 	}
1773 	const auto &active = _session->windows();
1774 	if (active.empty()) {
1775 		return;
1776 	}
1777 	const auto window = active.front();
1778 	auto &attachedStickers = _session->api().attachedStickers();
1779 	if (_photo) {
1780 		attachedStickers.requestAttachedStickerSets(window, _photo);
1781 	} else if (_document) {
1782 		attachedStickers.requestAttachedStickerSets(window, _document);
1783 	} else {
1784 		return;
1785 	}
1786 	close();
1787 }
1788 
sharedMediaType() const1789 auto OverlayWidget::sharedMediaType() const
1790 -> std::optional<SharedMediaType> {
1791 	using Type = SharedMediaType;
1792 	if (_message) {
1793 		if (const auto media = _message->media()) {
1794 			if (media->webpage()) {
1795 				return std::nullopt;
1796 			}
1797 		}
1798 		if (_photo) {
1799 			if (_message->isService()) {
1800 				return Type::ChatPhoto;
1801 			}
1802 			return Type::PhotoVideo;
1803 		} else if (_document) {
1804 			if (_document->isGifv()) {
1805 				return Type::GIF;
1806 			} else if (_document->isVideoFile()) {
1807 				return Type::PhotoVideo;
1808 			}
1809 			return Type::File;
1810 		}
1811 	}
1812 	return std::nullopt;
1813 }
1814 
sharedMediaKey() const1815 auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> {
1816 	if (!_message
1817 		&& _peer
1818 		&& !_user
1819 		&& _photo
1820 		&& _peer->userpicPhotoId() == _photo->id) {
1821 		return SharedMediaKey {
1822 			_history->peer->id,
1823 			_migrated ? _migrated->peer->id : 0,
1824 			SharedMediaType::ChatPhoto,
1825 			_photo
1826 		};
1827 	}
1828 	if (!_message) {
1829 		return std::nullopt;
1830 	}
1831 	const auto isScheduled = _message->isScheduled();
1832 	const auto keyForType = [&](SharedMediaType type) -> SharedMediaKey {
1833 		return {
1834 			_history->peer->id,
1835 			_migrated ? _migrated->peer->id : 0,
1836 			type,
1837 			(_message->history() == _history
1838 				? _message->id
1839 				: (_message->id - ServerMaxMsgId)),
1840 			isScheduled
1841 		};
1842 	};
1843 	if (!_message->isRegular() && !isScheduled) {
1844 		return std::nullopt;
1845 	}
1846 	return sharedMediaType() | keyForType;
1847 }
1848 
fileOrigin() const1849 Data::FileOrigin OverlayWidget::fileOrigin() const {
1850 	if (_message) {
1851 		return _message->fullId();
1852 	} else if (_photo && _user) {
1853 		return Data::FileOriginUserPhoto(peerToUser(_user->id), _photo->id);
1854 	} else if (_photo && _peer && _peer->userpicPhotoId() == _photo->id) {
1855 		return Data::FileOriginPeerPhoto(_peer->id);
1856 	}
1857 	return Data::FileOrigin();
1858 }
1859 
fileOrigin(const Entity & entity) const1860 Data::FileOrigin OverlayWidget::fileOrigin(const Entity &entity) const {
1861 	if (const auto item = entity.item) {
1862 		return item->fullId();
1863 	} else if (!v::is<not_null<PhotoData*>>(entity.data)) {
1864 		return Data::FileOrigin();
1865 	}
1866 	const auto photo = v::get<not_null<PhotoData*>>(entity.data);
1867 	if (_user) {
1868 		return Data::FileOriginUserPhoto(peerToUser(_user->id), photo->id);
1869 	} else if (_peer && _peer->userpicPhotoId() == photo->id) {
1870 		return Data::FileOriginPeerPhoto(_peer->id);
1871 	}
1872 	return Data::FileOrigin();
1873 }
1874 
validSharedMedia() const1875 bool OverlayWidget::validSharedMedia() const {
1876 	if (auto key = sharedMediaKey()) {
1877 		if (!_sharedMedia) {
1878 			return false;
1879 		}
1880 		using Key = SharedMediaWithLastSlice::Key;
1881 		auto inSameDomain = [](const Key &a, const Key &b) {
1882 			return (a.type == b.type)
1883 				&& (a.peerId == b.peerId)
1884 				&& (a.migratedPeerId == b.migratedPeerId)
1885 				&& (a.scheduled == b.scheduled);
1886 		};
1887 		auto countDistanceInData = [&](const Key &a, const Key &b) {
1888 			return [&](const SharedMediaWithLastSlice &data) {
1889 				return inSameDomain(a, b)
1890 					? data.distance(a, b)
1891 					: std::optional<int>();
1892 			};
1893 		};
1894 
1895 		if (key == _sharedMedia->key) {
1896 			return true;
1897 		} else if (!_sharedMediaDataKey
1898 			|| _sharedMedia->key != *_sharedMediaDataKey) {
1899 			return false;
1900 		}
1901 		auto distance = _sharedMediaData
1902 			| countDistanceInData(*key, _sharedMedia->key)
1903 			| func::abs;
1904 		if (distance) {
1905 			return (*distance < kIdsPreloadAfter);
1906 		}
1907 	}
1908 	return (_sharedMedia == nullptr);
1909 }
1910 
validateSharedMedia()1911 void OverlayWidget::validateSharedMedia() {
1912 	if (const auto key = sharedMediaKey()) {
1913 		Assert(_history != nullptr);
1914 
1915 		_sharedMedia = std::make_unique<SharedMedia>(*key);
1916 		auto viewer = (key->type == SharedMediaType::ChatPhoto)
1917 			? SharedMediaWithLastReversedViewer
1918 			: SharedMediaWithLastViewer;
1919 		viewer(
1920 			&_history->session(),
1921 			*key,
1922 			kIdsLimit,
1923 			kIdsLimit
1924 		) | rpl::start_with_next([this](
1925 				SharedMediaWithLastSlice &&update) {
1926 			handleSharedMediaUpdate(std::move(update));
1927 		}, _sharedMedia->lifetime);
1928 	} else {
1929 		_sharedMedia = nullptr;
1930 		_sharedMediaData = std::nullopt;
1931 		_sharedMediaDataKey = std::nullopt;
1932 	}
1933 }
1934 
handleSharedMediaUpdate(SharedMediaWithLastSlice && update)1935 void OverlayWidget::handleSharedMediaUpdate(SharedMediaWithLastSlice &&update) {
1936 	if ((!_photo && !_document) || !_sharedMedia) {
1937 		_sharedMediaData = std::nullopt;
1938 		_sharedMediaDataKey = std::nullopt;
1939 	} else {
1940 		_sharedMediaData = std::move(update);
1941 		_sharedMediaDataKey = _sharedMedia->key;
1942 	}
1943 	findCurrent();
1944 	updateControls();
1945 	preloadData(0);
1946 }
1947 
userPhotosKey() const1948 std::optional<OverlayWidget::UserPhotosKey> OverlayWidget::userPhotosKey() const {
1949 	if (!_message && _user && _photo) {
1950 		return UserPhotosKey{ peerToUser(_user->id), _photo->id };
1951 	}
1952 	return std::nullopt;
1953 }
1954 
validUserPhotos() const1955 bool OverlayWidget::validUserPhotos() const {
1956 	if (const auto key = userPhotosKey()) {
1957 		if (!_userPhotos) {
1958 			return false;
1959 		}
1960 		const auto countDistanceInData = [](const auto &a, const auto &b) {
1961 			return [&](const UserPhotosSlice &data) {
1962 				return data.distance(a, b);
1963 			};
1964 		};
1965 
1966 		const auto distance = (key == _userPhotos->key) ? 0 :
1967 			_userPhotosData
1968 			| countDistanceInData(*key, _userPhotos->key)
1969 			| func::abs;
1970 		if (distance) {
1971 			return (*distance < kIdsPreloadAfter);
1972 		}
1973 	}
1974 	return (_userPhotos == nullptr);
1975 }
1976 
validateUserPhotos()1977 void OverlayWidget::validateUserPhotos() {
1978 	if (const auto key = userPhotosKey()) {
1979 		Assert(_user != nullptr);
1980 
1981 		_userPhotos = std::make_unique<UserPhotos>(*key);
1982 		UserPhotosReversedViewer(
1983 			&_user->session(),
1984 			*key,
1985 			kIdsLimit,
1986 			kIdsLimit
1987 		) | rpl::start_with_next([this](
1988 				UserPhotosSlice &&update) {
1989 			handleUserPhotosUpdate(std::move(update));
1990 		}, _userPhotos->lifetime);
1991 	} else {
1992 		_userPhotos = nullptr;
1993 		_userPhotosData = std::nullopt;
1994 	}
1995 }
1996 
handleUserPhotosUpdate(UserPhotosSlice && update)1997 void OverlayWidget::handleUserPhotosUpdate(UserPhotosSlice &&update) {
1998 	if (!_photo || !_userPhotos) {
1999 		_userPhotosData = std::nullopt;
2000 	} else {
2001 		_userPhotosData = std::move(update);
2002 	}
2003 	findCurrent();
2004 	updateControls();
2005 	preloadData(0);
2006 }
2007 
collageKey() const2008 std::optional<OverlayWidget::CollageKey> OverlayWidget::collageKey() const {
2009 	if (_message) {
2010 		if (const auto media = _message->media()) {
2011 			if (const auto page = media->webpage()) {
2012 				for (const auto &item : page->collage.items) {
2013 					if (item == _photo || item == _document) {
2014 						return item;
2015 					}
2016 				}
2017 			}
2018 		}
2019 	}
2020 	return std::nullopt;
2021 }
2022 
validCollage() const2023 bool OverlayWidget::validCollage() const {
2024 	if (const auto key = collageKey()) {
2025 		if (!_collage) {
2026 			return false;
2027 		}
2028 
2029 		if (key == _collage->key) {
2030 			return true;
2031 		} else if (_collageData) {
2032 			const auto &items = _collageData->items;
2033 			if (ranges::find(items, *key) != end(items)
2034 				&& ranges::find(items, _collage->key) != end(items)) {
2035 				return true;
2036 			}
2037 		}
2038 	}
2039 	return (_collage == nullptr);
2040 }
2041 
validateCollage()2042 void OverlayWidget::validateCollage() {
2043 	if (const auto key = collageKey()) {
2044 		_collage = std::make_unique<Collage>(*key);
2045 		_collageData = WebPageCollage();
2046 		if (_message) {
2047 			if (const auto media = _message->media()) {
2048 				if (const auto page = media->webpage()) {
2049 					_collageData = page->collage;
2050 				}
2051 			}
2052 		}
2053 	} else {
2054 		_collage = nullptr;
2055 		_collageData = std::nullopt;
2056 	}
2057 }
2058 
refreshMediaViewer()2059 void OverlayWidget::refreshMediaViewer() {
2060 	if (!validSharedMedia()) {
2061 		validateSharedMedia();
2062 	}
2063 	if (!validUserPhotos()) {
2064 		validateUserPhotos();
2065 	}
2066 	if (!validCollage()) {
2067 		validateCollage();
2068 	}
2069 	findCurrent();
2070 	updateControls();
2071 }
2072 
refreshFromLabel()2073 void OverlayWidget::refreshFromLabel() {
2074 	if (_message) {
2075 		_from = _message->senderOriginal();
2076 		if (const auto info = _message->hiddenForwardedInfo()) {
2077 			_fromName = info->name;
2078 		} else {
2079 			Assert(_from != nullptr);
2080 			const auto from = _from->migrateTo() ? _from->migrateTo() : _from;
2081 			_fromName = from->name;
2082 		}
2083 	} else {
2084 		_from = _user;
2085 		_fromName = _user ? _user->name : QString();
2086 	}
2087 }
2088 
refreshCaption()2089 void OverlayWidget::refreshCaption() {
2090 	_caption = Ui::Text::String();
2091 	if (!_message) {
2092 		return;
2093 	} else if (const auto media = _message->media()) {
2094 		if (media->webpage()) {
2095 			return;
2096 		}
2097 	}
2098 	const auto caption = _message->originalText();
2099 	if (caption.text.isEmpty()) {
2100 		return;
2101 	}
2102 
2103 	using namespace HistoryView;
2104 	_caption = Ui::Text::String(st::msgMinWidth);
2105 	const auto duration = (_streamed && _document && !videoIsGifOrUserpic())
2106 		? _document->getDuration()
2107 		: 0;
2108 	const auto base = duration
2109 		? DocumentTimestampLinkBase(_document, _message->fullId())
2110 		: QString();
2111 	const auto context = Core::MarkedTextContext{
2112 		.session = &_message->history()->session()
2113 	};
2114 	_caption.setMarkedText(
2115 		st::mediaviewCaptionStyle,
2116 		AddTimestampLinks(caption, duration, base),
2117 		Ui::ItemTextOptions(_message),
2118 		context);
2119 }
2120 
refreshGroupThumbs()2121 void OverlayWidget::refreshGroupThumbs() {
2122 	const auto existed = (_groupThumbs != nullptr);
2123 	if (_index && _sharedMediaData) {
2124 		View::GroupThumbs::Refresh(
2125 			_session,
2126 			_groupThumbs,
2127 			*_sharedMediaData,
2128 			*_index,
2129 			_groupThumbsAvailableWidth);
2130 	} else if (_index && _userPhotosData) {
2131 		View::GroupThumbs::Refresh(
2132 			_session,
2133 			_groupThumbs,
2134 			*_userPhotosData,
2135 			*_index,
2136 			_groupThumbsAvailableWidth);
2137 	} else if (_index && _collageData) {
2138 		const auto messageId = _message ? _message->fullId() : FullMsgId();
2139 		View::GroupThumbs::Refresh(
2140 			_session,
2141 			_groupThumbs,
2142 			{ messageId, &*_collageData },
2143 			*_index,
2144 			_groupThumbsAvailableWidth);
2145 	} else if (_groupThumbs) {
2146 		_groupThumbs->clear();
2147 		_groupThumbs->resizeToWidth(_groupThumbsAvailableWidth);
2148 	}
2149 	if (_groupThumbs && !existed) {
2150 		initGroupThumbs();
2151 	}
2152 }
2153 
initGroupThumbs()2154 void OverlayWidget::initGroupThumbs() {
2155 	Expects(_groupThumbs != nullptr);
2156 
2157 	_groupThumbs->updateRequests(
2158 	) | rpl::start_with_next([this](QRect rect) {
2159 		const auto shift = (width() / 2);
2160 		_groupThumbsRect = QRect(
2161 			shift + rect.x(),
2162 			_groupThumbsTop,
2163 			rect.width(),
2164 			_groupThumbs->height());
2165 		update(_groupThumbsRect);
2166 	}, _groupThumbs->lifetime());
2167 
2168 	_groupThumbs->activateRequests(
2169 	) | rpl::start_with_next([this](View::GroupThumbs::Key key) {
2170 		using CollageKey = View::GroupThumbs::CollageKey;
2171 		if (const auto photoId = std::get_if<PhotoId>(&key)) {
2172 			const auto photo = _session->data().photo(*photoId);
2173 			moveToEntity({ photo, nullptr });
2174 		} else if (const auto itemId = std::get_if<FullMsgId>(&key)) {
2175 			moveToEntity(entityForItemId(*itemId));
2176 		} else if (const auto collageKey = std::get_if<CollageKey>(&key)) {
2177 			if (_collageData) {
2178 				moveToEntity(entityForCollage(collageKey->index));
2179 			}
2180 		}
2181 	}, _groupThumbs->lifetime());
2182 
2183 	_groupThumbsRect = QRect(
2184 		_groupThumbsLeft,
2185 		_groupThumbsTop,
2186 		width() - 2 * _groupThumbsLeft,
2187 		height() - _groupThumbsTop);
2188 }
2189 
clearControlsState()2190 void OverlayWidget::clearControlsState() {
2191 	_saveMsgStarted = 0;
2192 	_loadRequest = 0;
2193 	_over = _down = OverNone;
2194 	_pressed = false;
2195 	_dragging = 0;
2196 	setCursor(style::cur_default);
2197 	if (!_animations.empty()) {
2198 		_animations.clear();
2199 		_stateAnimation.stop();
2200 	}
2201 	if (!_animationOpacities.empty()) {
2202 		_animationOpacities.clear();
2203 	}
2204 }
2205 
window() const2206 not_null<QWindow*> OverlayWidget::window() const {
2207 	return _widget->windowHandle();
2208 }
2209 
width() const2210 int OverlayWidget::width() const {
2211 	return _widget->width();
2212 }
2213 
height() const2214 int OverlayWidget::height() const {
2215 	return _widget->height();
2216 }
2217 
update()2218 void OverlayWidget::update() {
2219 	_widget->update();
2220 }
2221 
update(const QRegion & region)2222 void OverlayWidget::update(const QRegion &region) {
2223 	_widget->update(region);
2224 }
2225 
isHidden() const2226 bool OverlayWidget::isHidden() const {
2227 	return _widget->isHidden();
2228 }
2229 
widget() const2230 not_null<QWidget*> OverlayWidget::widget() const {
2231 	return _widget;
2232 }
2233 
hide()2234 void OverlayWidget::hide() {
2235 	clearBeforeHide();
2236 	applyHideWindowWorkaround();
2237 	_widget->hide();
2238 }
2239 
setCursor(style::cursor cursor)2240 void OverlayWidget::setCursor(style::cursor cursor) {
2241 	_widget->setCursor(cursor);
2242 }
2243 
setFocus()2244 void OverlayWidget::setFocus() {
2245 	_widget->setFocus();
2246 }
2247 
activate()2248 void OverlayWidget::activate() {
2249 	_widget->raise();
2250 	_widget->activateWindow();
2251 	QApplication::setActiveWindow(_widget);
2252 	setFocus();
2253 }
2254 
show(OpenRequest request)2255 void OverlayWidget::show(OpenRequest request) {
2256 	const auto document = request.document();
2257 	const auto photo = request.photo();
2258 	const auto contextItem = request.item();
2259 	const auto contextPeer = request.peer();
2260 	if (photo) {
2261 		if (contextItem && contextPeer) {
2262 			return;
2263 		}
2264 		setSession(&photo->session());
2265 
2266 		if (contextPeer) {
2267 			setContext(contextPeer);
2268 		} else if (contextItem) {
2269 			setContext(contextItem);
2270 		} else {
2271 			setContext(v::null);
2272 		}
2273 
2274 		clearControlsState();
2275 		_firstOpenedPeerPhoto = (contextPeer != nullptr);
2276 		assignMediaPointer(photo);
2277 
2278 		displayPhoto(photo);
2279 		preloadData(0);
2280 		activateControls();
2281 	} else if (document) {
2282 		setSession(&document->session());
2283 
2284 		if (contextItem) {
2285 			setContext(contextItem);
2286 		} else {
2287 			setContext(v::null);
2288 		}
2289 
2290 		clearControlsState();
2291 
2292 		_streamingStartPaused = false;
2293 		displayDocument(
2294 			document,
2295 			request.cloudTheme()
2296 				? *request.cloudTheme()
2297 				: Data::CloudTheme(),
2298 			request.continueStreaming());
2299 		if (!isHidden()) {
2300 			preloadData(0);
2301 			activateControls();
2302 		}
2303 	}
2304 	if (const auto controller = request.controller()) {
2305 		_window = base::make_weak(&controller->window());
2306 	}
2307 }
2308 
displayPhoto(not_null<PhotoData * > photo)2309 void OverlayWidget::displayPhoto(not_null<PhotoData*> photo) {
2310 	if (photo->isNull()) {
2311 		displayDocument(nullptr);
2312 		return;
2313 	}
2314 	_touchbarDisplay.fire(TouchBarItemType::Photo);
2315 
2316 	clearStreaming();
2317 	destroyThemePreview();
2318 
2319 	_fullScreenVideo = false;
2320 	assignMediaPointer(photo);
2321 	_rotation = _photo->owner().mediaRotation().get(_photo);
2322 	_radial.stop();
2323 
2324 	refreshMediaViewer();
2325 
2326 	_staticContent = QImage();
2327 	if (_photo->videoCanBePlayed()) {
2328 		initStreaming();
2329 	}
2330 
2331 	refreshCaption();
2332 
2333 	_blurred = true;
2334 	_down = OverNone;
2335 	if (!_staticContent.isNull()) {
2336 		// Video thumbnail.
2337 		const auto size = style::ConvertScale(
2338 			flipSizeByRotation(_staticContent.size()));
2339 		_w = size.width();
2340 		_h = size.height();
2341 	} else {
2342 		const auto size = style::ConvertScale(flipSizeByRotation(QSize(
2343 			photo->width(),
2344 			photo->height())));
2345 		_w = size.width();
2346 		_h = size.height();
2347 	}
2348 	contentSizeChanged();
2349 	refreshFromLabel();
2350 	displayFinished();
2351 }
2352 
destroyThemePreview()2353 void OverlayWidget::destroyThemePreview() {
2354 	_themePreviewId = 0;
2355 	_themePreviewShown = false;
2356 	_themePreview.reset();
2357 	_themeApply.destroy();
2358 	_themeCancel.destroy();
2359 	_themeShare.destroy();
2360 }
2361 
redisplayContent()2362 void OverlayWidget::redisplayContent() {
2363 	if (isHidden() || !_session) {
2364 		return;
2365 	} else if (_photo) {
2366 		displayPhoto(_photo);
2367 	} else {
2368 		displayDocument(_document);
2369 	}
2370 }
2371 
2372 // Empty messages shown as docs: doc can be nullptr.
displayDocument(DocumentData * doc,const Data::CloudTheme & cloud,bool continueStreaming)2373 void OverlayWidget::displayDocument(
2374 		DocumentData *doc,
2375 		const Data::CloudTheme &cloud,
2376 		bool continueStreaming) {
2377 	_fullScreenVideo = false;
2378 	_staticContent = QImage();
2379 	clearStreaming(_document != doc);
2380 	destroyThemePreview();
2381 	assignMediaPointer(doc);
2382 
2383 	_rotation = _document
2384 		? _document->owner().mediaRotation().get(_document)
2385 		: 0;
2386 	_themeCloudData = cloud;
2387 	_radial.stop();
2388 
2389 	_touchbarDisplay.fire(TouchBarItemType::None);
2390 
2391 	refreshMediaViewer();
2392 	if (_document) {
2393 		if (_document->sticker()) {
2394 			if (const auto image = _documentMedia->getStickerLarge()) {
2395 				setStaticContent(image->original());
2396 			} else if (const auto thumbnail = _documentMedia->thumbnail()) {
2397 				setStaticContent(thumbnail->pixBlurred(
2398 					_document->dimensions.width(),
2399 					_document->dimensions.height()
2400 				).toImage());
2401 			}
2402 		} else {
2403 			if (_documentMedia->canBePlayed()
2404 				&& initStreaming(continueStreaming)) {
2405 			} else if (_document->isVideoFile()) {
2406 				_documentMedia->automaticLoad(fileOrigin(), _message);
2407 				initStreamingThumbnail();
2408 			} else if (_document->isTheme()) {
2409 				_documentMedia->automaticLoad(fileOrigin(), _message);
2410 				initThemePreview();
2411 			} else {
2412 				_documentMedia->automaticLoad(fileOrigin(), _message);
2413 				_document->saveFromDataSilent();
2414 				auto &location = _document->location(true);
2415 				if (location.accessEnable()) {
2416 					setStaticContent(PrepareStaticImage({
2417 						.path = location.name(),
2418 					}));
2419 					if (!_staticContent.isNull()) {
2420 						_touchbarDisplay.fire(TouchBarItemType::Photo);
2421 					}
2422 				} else {
2423 					setStaticContent(PrepareStaticImage({
2424 						.content = _documentMedia->bytes(),
2425 					}));
2426 					if (!_staticContent.isNull()) {
2427 						_touchbarDisplay.fire(TouchBarItemType::Photo);
2428 					}
2429 				}
2430 				location.accessDisable();
2431 			}
2432 		}
2433 	}
2434 	refreshCaption();
2435 
2436 	const auto docGeneric = Layout::DocumentGenericPreview::Create(_document);
2437 	_docExt = docGeneric.ext;
2438 	_docIconColor = docGeneric.color;
2439 	_docIcon = docGeneric.icon();
2440 
2441 	int32 extmaxw = (st::mediaviewFileIconSize - st::mediaviewFileExtPadding * 2);
2442 	_docExtWidth = st::mediaviewFileExtFont->width(_docExt);
2443 	if (_docExtWidth > extmaxw) {
2444 		_docExt = st::mediaviewFileExtFont->elided(_docExt, extmaxw, Qt::ElideMiddle);
2445 		_docExtWidth = st::mediaviewFileExtFont->width(_docExt);
2446 	}
2447 	if (documentBubbleShown()) {
2448 		if (_document && _document->hasThumbnail()) {
2449 			_document->loadThumbnail(fileOrigin());
2450 			const auto tw = _documentMedia->thumbnailSize().width();
2451 			const auto th = _documentMedia->thumbnailSize().height();
2452 			if (!tw || !th) {
2453 				_docThumbx = _docThumby = _docThumbw = 0;
2454 			} else if (tw > th) {
2455 				_docThumbw = (tw * st::mediaviewFileIconSize) / th;
2456 				_docThumbx = (_docThumbw - st::mediaviewFileIconSize) / 2;
2457 				_docThumby = 0;
2458 			} else {
2459 				_docThumbw = st::mediaviewFileIconSize;
2460 				_docThumbx = 0;
2461 				_docThumby = ((th * _docThumbw) / tw - st::mediaviewFileIconSize) / 2;
2462 			}
2463 		}
2464 
2465 		int32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;
2466 
2467 		if (_document) {
2468 			_docName = (_document->type == StickerDocument)
2469 				? tr::lng_in_dlg_sticker(tr::now)
2470 				: (_document->type == AnimatedDocument
2471 					? qsl("GIF")
2472 					: (_document->filename().isEmpty()
2473 						? tr::lng_mediaview_doc_image(tr::now)
2474 						: _document->filename()));
2475 		} else {
2476 			_docName = tr::lng_message_empty(tr::now);
2477 		}
2478 		_docNameWidth = st::mediaviewFileNameFont->width(_docName);
2479 		if (_docNameWidth > maxw) {
2480 			_docName = st::mediaviewFileNameFont->elided(_docName, maxw, Qt::ElideMiddle);
2481 			_docNameWidth = st::mediaviewFileNameFont->width(_docName);
2482 		}
2483 	} else if (_themePreviewShown) {
2484 		updateThemePreviewGeometry();
2485 	} else if (!_staticContent.isNull()) {
2486 		const auto size = style::ConvertScale(
2487 			flipSizeByRotation(_staticContent.size()));
2488 		_w = size.width();
2489 		_h = size.height();
2490 	} else if (videoShown()) {
2491 		const auto contentSize = style::ConvertScale(videoSize());
2492 		_w = contentSize.width();
2493 		_h = contentSize.height();
2494 	}
2495 	contentSizeChanged();
2496 	if (videoShown()) {
2497 		applyVideoSize();
2498 	}
2499 	refreshFromLabel();
2500 	_blurred = false;
2501 	if (_showAsPip && _streamed && !videoIsGifOrUserpic()) {
2502 		switchToPip();
2503 	} else {
2504 		displayFinished();
2505 	}
2506 }
2507 
updateThemePreviewGeometry()2508 void OverlayWidget::updateThemePreviewGeometry() {
2509 	if (_themePreviewShown) {
2510 		auto previewRect = QRect((width() - st::themePreviewSize.width()) / 2, (height() - st::themePreviewSize.height()) / 2, st::themePreviewSize.width(), st::themePreviewSize.height());
2511 		_themePreviewRect = previewRect.marginsAdded(st::themePreviewMargin);
2512 		if (_themeApply) {
2513 			auto right = qMax(width() - _themePreviewRect.x() - _themePreviewRect.width(), 0) + st::themePreviewMargin.right();
2514 			auto bottom = qMin(height(), _themePreviewRect.y() + _themePreviewRect.height());
2515 			_themeApply->moveToRight(right, bottom - st::themePreviewMargin.bottom() + (st::themePreviewMargin.bottom() - _themeApply->height()) / 2);
2516 			right += _themeApply->width() + st::themePreviewButtonsSkip;
2517 			_themeCancel->moveToRight(right, _themeApply->y());
2518 			if (_themeShare) {
2519 				_themeShare->moveToLeft(previewRect.x(), _themeApply->y());
2520 			}
2521 		}
2522 
2523 		// For context menu event.
2524 		_x = _themePreviewRect.x();
2525 		_y = _themePreviewRect.y();
2526 		_w = _themePreviewRect.width();
2527 		_h = _themePreviewRect.height();
2528 	}
2529 }
2530 
displayFinished()2531 void OverlayWidget::displayFinished() {
2532 	updateControls();
2533 	if (isHidden()) {
2534 		moveToScreen();
2535 		//setAttribute(Qt::WA_DontShowOnScreen);
2536 		//OverlayParent::setVisibleHook(true);
2537 		//OverlayParent::setVisibleHook(false);
2538 		//setAttribute(Qt::WA_DontShowOnScreen, false);
2539 		Ui::Platform::UpdateOverlayed(_widget);
2540 		if (Platform::IsLinux()) {
2541 			_widget->showFullScreen();
2542 		} else {
2543 			_widget->show();
2544 		}
2545 		Ui::Platform::ShowOverAll(_widget);
2546 		activate();
2547 	}
2548 }
2549 
canInitStreaming() const2550 bool OverlayWidget::canInitStreaming() const {
2551 	return (_document && _documentMedia->canBePlayed())
2552 		|| (_photo && _photo->videoCanBePlayed());
2553 }
2554 
initStreaming(bool continueStreaming)2555 bool OverlayWidget::initStreaming(bool continueStreaming) {
2556 	Expects(canInitStreaming());
2557 
2558 	if (_streamed) {
2559 		return true;
2560 	}
2561 	initStreamingThumbnail();
2562 	if (!createStreamingObjects()) {
2563 		if (_document) {
2564 			_document->setInappPlaybackFailed();
2565 		} else {
2566 			_photo->setVideoPlaybackFailed();
2567 		}
2568 		return false;
2569 	}
2570 
2571 	Core::App().updateNonIdle();
2572 
2573 	_streamed->instance.player().updates(
2574 	) | rpl::start_with_next_error([=](Streaming::Update &&update) {
2575 		handleStreamingUpdate(std::move(update));
2576 	}, [=](Streaming::Error &&error) {
2577 		handleStreamingError(std::move(error));
2578 	}, _streamed->instance.lifetime());
2579 
2580 	if (continueStreaming) {
2581 		_pip = nullptr;
2582 	}
2583 	if (!continueStreaming
2584 		|| (!_streamed->instance.player().active()
2585 			&& !_streamed->instance.player().finished())) {
2586 		startStreamingPlayer();
2587 	} else {
2588 		updatePlaybackState();
2589 	}
2590 	return true;
2591 }
2592 
startStreamingPlayer()2593 void OverlayWidget::startStreamingPlayer() {
2594 	Expects(_streamed != nullptr);
2595 
2596 	const auto &player = _streamed->instance.player();
2597 	if (player.playing()) {
2598 		if (!_streamed->withSound) {
2599 			return;
2600 		}
2601 		_pip = nullptr;
2602 	} else if (!player.paused() && !player.finished() && !player.failed()) {
2603 		_pip = nullptr;
2604 	} else if (_pip && _streamed->withSound) {
2605 		return;
2606 	}
2607 
2608 	const auto position = _document
2609 		? _document->session().settings().mediaLastPlaybackPosition(
2610 			_document->id)
2611 		: _photo
2612 		? _photo->videoStartPosition()
2613 		: 0;
2614 	restartAtSeekPosition(position);
2615 }
2616 
initStreamingThumbnail()2617 void OverlayWidget::initStreamingThumbnail() {
2618 	Expects(_photo || _document);
2619 
2620 	_touchbarDisplay.fire(TouchBarItemType::Video);
2621 
2622 	const auto computePhotoThumbnail = [&] {
2623 		const auto thumbnail = _photoMedia->image(Data::PhotoSize::Thumbnail);
2624 		if (thumbnail) {
2625 			return thumbnail;
2626 		} else if (_peer && _peer->userpicPhotoId() == _photo->id) {
2627 			if (const auto view = _peer->activeUserpicView()) {
2628 				if (const auto image = view->image()) {
2629 					return image;
2630 				}
2631 			}
2632 		}
2633 		return thumbnail;
2634 	};
2635 	const auto good = _document
2636 		? _documentMedia->goodThumbnail()
2637 		: _photoMedia->image(Data::PhotoSize::Large);
2638 	const auto thumbnail = _document
2639 		? _documentMedia->thumbnail()
2640 		: computePhotoThumbnail();
2641 	const auto blurred = _document
2642 		? _documentMedia->thumbnailInline()
2643 		: _photoMedia->thumbnailInline();
2644 	const auto size = _photo
2645 		? QSize(
2646 			_photo->videoLocation().width(),
2647 			_photo->videoLocation().height())
2648 		: good
2649 		? good->size()
2650 		: _document->dimensions;
2651 	if (!good && !thumbnail && !blurred) {
2652 		return;
2653 	} else if (size.isEmpty()) {
2654 		return;
2655 	}
2656 	const auto w = size.width();
2657 	const auto h = size.height();
2658 	const auto options = VideoThumbOptions(_document);
2659 	const auto goodOptions = (options & ~Images::Option::Blurred);
2660 	setStaticContent((good
2661 		? good
2662 		: thumbnail
2663 		? thumbnail
2664 		: blurred
2665 		? blurred
2666 		: Image::BlankMedia().get())->pixNoCache(
2667 			w,
2668 			h,
2669 			good ? goodOptions : options,
2670 			w / cIntRetinaFactor(),
2671 			h / cIntRetinaFactor()
2672 		).toImage());
2673 }
2674 
streamingReady(Streaming::Information && info)2675 void OverlayWidget::streamingReady(Streaming::Information &&info) {
2676 	if (videoShown()) {
2677 		applyVideoSize();
2678 	} else {
2679 		updateContentRect();
2680 	}
2681 }
2682 
applyVideoSize()2683 void OverlayWidget::applyVideoSize() {
2684 	const auto contentSize = style::ConvertScale(videoSize());
2685 	if (contentSize != QSize(_width, _height)) {
2686 		updateContentRect();
2687 		_w = contentSize.width();
2688 		_h = contentSize.height();
2689 		contentSizeChanged();
2690 	}
2691 	updateContentRect();
2692 }
2693 
createStreamingObjects()2694 bool OverlayWidget::createStreamingObjects() {
2695 	Expects(_photo || _document);
2696 
2697 	if (_document) {
2698 		_streamed = std::make_unique<Streamed>(
2699 			_document,
2700 			fileOrigin(),
2701 			_widget,
2702 			static_cast<PlaybackControls::Delegate*>(this),
2703 			[=] { waitingAnimationCallback(); });
2704 	} else {
2705 		_streamed = std::make_unique<Streamed>(
2706 			_photo,
2707 			fileOrigin(),
2708 			_widget,
2709 			static_cast<PlaybackControls::Delegate*>(this),
2710 			[=] { waitingAnimationCallback(); });
2711 	}
2712 	if (!_streamed->instance.valid()) {
2713 		_streamed = nullptr;
2714 		return false;
2715 	}
2716 	++_streamedCreated;
2717 	_streamed->instance.setPriority(kOverlayLoaderPriority);
2718 	_streamed->instance.lockPlayer();
2719 	_streamed->withSound = _document
2720 		&& (_document->isAudioFile()
2721 			|| _document->isVideoFile()
2722 			|| _document->isVoiceMessage()
2723 			|| _document->isVideoMessage());
2724 
2725 	if (videoIsGifOrUserpic()) {
2726 		_streamed->controls.hide();
2727 	} else {
2728 		refreshClipControllerGeometry();
2729 		_streamed->controls.show();
2730 	}
2731 	return true;
2732 }
2733 
transformedShownContent() const2734 QImage OverlayWidget::transformedShownContent() const {
2735 	return transformShownContent(
2736 		videoShown() ? currentVideoFrameImage() : _staticContent,
2737 		finalContentRotation());
2738 }
2739 
transformShownContent(QImage content,int rotation) const2740 QImage OverlayWidget::transformShownContent(
2741 		QImage content,
2742 		int rotation) const {
2743 	if (rotation) {
2744 		content = RotateFrameImage(std::move(content), rotation);
2745 	}
2746 	if (videoShown()) {
2747 		const auto requiredSize = videoSize();
2748 		if (content.size() != requiredSize) {
2749 			content = content.scaled(
2750 				requiredSize,
2751 				Qt::IgnoreAspectRatio,
2752 				Qt::SmoothTransformation);
2753 		}
2754 	}
2755 	return content;
2756 }
2757 
handleStreamingUpdate(Streaming::Update && update)2758 void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
2759 	using namespace Streaming;
2760 
2761 	v::match(update.data, [&](Information &update) {
2762 		streamingReady(std::move(update));
2763 	}, [&](const PreloadedVideo &update) {
2764 		updatePlaybackState();
2765 	}, [&](const UpdateVideo &update) {
2766 		updateContentRect();
2767 		Core::App().updateNonIdle();
2768 		updatePlaybackState();
2769 	}, [&](const PreloadedAudio &update) {
2770 		updatePlaybackState();
2771 	}, [&](const UpdateAudio &update) {
2772 		updatePlaybackState();
2773 	}, [&](WaitingForData) {
2774 	}, [&](MutedByOther) {
2775 	}, [&](Finished) {
2776 		updatePlaybackState();
2777 	});
2778 }
2779 
handleStreamingError(Streaming::Error && error)2780 void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
2781 	Expects(_document || _photo);
2782 
2783 	if (error == Streaming::Error::NotStreamable) {
2784 		if (_document) {
2785 			_document->setNotSupportsStreaming();
2786 		} else {
2787 			_photo->setVideoPlaybackFailed();
2788 		}
2789 	} else if (error == Streaming::Error::OpenFailed) {
2790 		if (_document) {
2791 			_document->setInappPlaybackFailed();
2792 		} else {
2793 			_photo->setVideoPlaybackFailed();
2794 		}
2795 	}
2796 	if (canInitStreaming()) {
2797 		updatePlaybackState();
2798 	} else {
2799 		redisplayContent();
2800 	}
2801 }
2802 
initThemePreview()2803 void OverlayWidget::initThemePreview() {
2804 	using namespace Window::Theme;
2805 
2806 	Assert(_document && _document->isTheme());
2807 
2808 	const auto bytes = _documentMedia->bytes();
2809 	auto &location = _document->location();
2810 	if (bytes.isEmpty()
2811 		&& (location.isEmpty() || !location.accessEnable())) {
2812 		return;
2813 	}
2814 	_themePreviewShown = true;
2815 
2816 	auto current = CurrentData();
2817 	current.backgroundId = Background()->id();
2818 	current.backgroundImage = Background()->createCurrentImage();
2819 	current.backgroundTiled = Background()->tile();
2820 
2821 	const auto &cloudList = _document->session().data().cloudThemes().list();
2822 	const auto i = ranges::find(
2823 		cloudList,
2824 		_document->id,
2825 		&Data::CloudTheme::documentId);
2826 	const auto cloud = (i != end(cloudList)) ? *i : Data::CloudTheme();
2827 	const auto isTrusted = (cloud.documentId != 0);
2828 	const auto fields = [&] {
2829 		auto result = _themeCloudData.id ? _themeCloudData : cloud;
2830 		if (!result.documentId) {
2831 			result.documentId = _document->id;
2832 		}
2833 		return result;
2834 	}();
2835 
2836 	const auto weakSession = base::make_weak(&_document->session());
2837 	const auto path = _document->location().name();
2838 	const auto id = _themePreviewId = base::RandomValue<uint64>();
2839 	const auto weak = Ui::MakeWeak(_widget);
2840 	crl::async([=, data = std::move(current)]() mutable {
2841 		auto preview = GeneratePreview(
2842 			bytes,
2843 			path,
2844 			fields,
2845 			std::move(data),
2846 			Window::Theme::PreviewType::Extended);
2847 		crl::on_main(weak, [=, result = std::move(preview)]() mutable {
2848 			const auto session = weakSession.get();
2849 			if (id != _themePreviewId || !session) {
2850 				return;
2851 			}
2852 			_themePreviewId = 0;
2853 			_themePreview = std::move(result);
2854 			if (_themePreview) {
2855 				_themeApply.create(
2856 					_widget,
2857 					tr::lng_theme_preview_apply(),
2858 					st::themePreviewApplyButton);
2859 				_themeApply->show();
2860 				_themeApply->setClickedCallback([=] {
2861 					const auto &object = Background()->themeObject();
2862 					const auto currentlyIsCustom = !object.cloud.id
2863 						&& !IsEmbeddedTheme(object.pathAbsolute);
2864 					auto preview = std::move(_themePreview);
2865 					close();
2866 					Apply(std::move(preview));
2867 					if (isTrusted && !currentlyIsCustom) {
2868 						KeepApplied();
2869 					}
2870 				});
2871 				_themeCancel.create(
2872 					_widget,
2873 					tr::lng_cancel(),
2874 					st::themePreviewCancelButton);
2875 				_themeCancel->show();
2876 				_themeCancel->setClickedCallback([this] { close(); });
2877 				if (const auto slug = _themeCloudData.slug; !slug.isEmpty()) {
2878 					_themeShare.create(
2879 						_widget,
2880 						tr::lng_theme_share(),
2881 						st::themePreviewCancelButton);
2882 					_themeShare->show();
2883 					_themeShare->setClickedCallback([=] {
2884 						QGuiApplication::clipboard()->setText(
2885 							session->createInternalLinkFull("addtheme/" + slug));
2886 						Ui::Toast::Show(
2887 							_widget,
2888 							tr::lng_background_link_copied(tr::now));
2889 					});
2890 				} else {
2891 					_themeShare.destroy();
2892 				}
2893 				updateControls();
2894 			}
2895 			update();
2896 		});
2897 	});
2898 	location.accessDisable();
2899 }
2900 
refreshClipControllerGeometry()2901 void OverlayWidget::refreshClipControllerGeometry() {
2902 	if (!_streamed || videoIsGifOrUserpic()) {
2903 		return;
2904 	}
2905 
2906 	if (_groupThumbs && _groupThumbs->hiding()) {
2907 		_groupThumbs = nullptr;
2908 		_groupThumbsRect = QRect();
2909 	}
2910 	const auto controllerBottom = _groupThumbs
2911 		? _groupThumbsTop
2912 		: height();
2913 	_streamed->controls.resize(st::mediaviewControllerSize);
2914 	_streamed->controls.move(
2915 		(width() - _streamed->controls.width()) / 2,
2916 		controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height());
2917 	Ui::SendPendingMoveResizeEvents(&_streamed->controls);
2918 }
2919 
playbackControlsPlay()2920 void OverlayWidget::playbackControlsPlay() {
2921 	playbackPauseResume();
2922 }
2923 
playbackControlsPause()2924 void OverlayWidget::playbackControlsPause() {
2925 	playbackPauseResume();
2926 }
2927 
playbackControlsToFullScreen()2928 void OverlayWidget::playbackControlsToFullScreen() {
2929 	playbackToggleFullScreen();
2930 }
2931 
playbackControlsFromFullScreen()2932 void OverlayWidget::playbackControlsFromFullScreen() {
2933 	playbackToggleFullScreen();
2934 }
2935 
playbackControlsToPictureInPicture()2936 void OverlayWidget::playbackControlsToPictureInPicture() {
2937 	if (!videoIsGifOrUserpic()) {
2938 		switchToPip();
2939 	}
2940 }
2941 
playbackControlsRotate()2942 void OverlayWidget::playbackControlsRotate() {
2943 	_oldGeometry = contentGeometry();
2944 	_geometryAnimation.stop();
2945 	if (_photo) {
2946 		auto &storage = _photo->owner().mediaRotation();
2947 		storage.set(_photo, storage.get(_photo) - 90);
2948 		_rotation = storage.get(_photo);
2949 		redisplayContent();
2950 	} else if (_document) {
2951 		auto &storage = _document->owner().mediaRotation();
2952 		storage.set(_document, storage.get(_document) - 90);
2953 		_rotation = storage.get(_document);
2954 		if (videoShown()) {
2955 			applyVideoSize();
2956 		} else {
2957 			redisplayContent();
2958 		}
2959 	}
2960 	if (_opengl) {
2961 		_geometryAnimation.start(
2962 			[=] { update(); },
2963 			0.,
2964 			1.,
2965 			st::widgetFadeDuration/*,
2966 			st::easeOutCirc*/);
2967 	}
2968 }
2969 
playbackPauseResume()2970 void OverlayWidget::playbackPauseResume() {
2971 	Expects(_streamed != nullptr);
2972 
2973 	_streamed->resumeOnCallEnd = false;
2974 	if (_streamed->instance.player().failed()) {
2975 		clearStreaming();
2976 		if (!canInitStreaming() || !initStreaming()) {
2977 			redisplayContent();
2978 		}
2979 	} else if (_streamed->instance.player().finished()
2980 		|| !_streamed->instance.player().active()) {
2981 		_streamingStartPaused = false;
2982 		restartAtSeekPosition(0);
2983 	} else if (_streamed->instance.player().paused()) {
2984 		_streamed->instance.resume();
2985 		updatePlaybackState();
2986 		playbackPauseMusic();
2987 	} else {
2988 		_streamed->instance.pause();
2989 		updatePlaybackState();
2990 	}
2991 }
2992 
seekRelativeTime(crl::time time)2993 void OverlayWidget::seekRelativeTime(crl::time time) {
2994 	Expects(_streamed != nullptr);
2995 
2996 	const auto newTime = std::clamp(
2997 		_streamed->instance.info().video.state.position + time,
2998 		crl::time(0),
2999 		_streamed->instance.info().video.state.duration);
3000 	restartAtSeekPosition(newTime);
3001 }
3002 
restartAtProgress(float64 progress)3003 void OverlayWidget::restartAtProgress(float64 progress) {
3004 	Expects(_streamed != nullptr);
3005 
3006 	restartAtSeekPosition(_streamed->instance.info().video.state.duration
3007 		* std::clamp(progress, 0., 1.));
3008 }
3009 
restartAtSeekPosition(crl::time position)3010 void OverlayWidget::restartAtSeekPosition(crl::time position) {
3011 	Expects(_streamed != nullptr);
3012 
3013 	if (videoShown()) {
3014 		_streamed->instance.saveFrameToCover();
3015 		const auto saved = base::take(_rotation);
3016 		setStaticContent(transformedShownContent());
3017 		_rotation = saved;
3018 		updateContentRect();
3019 	}
3020 	auto options = Streaming::PlaybackOptions();
3021 	options.position = position;
3022 	if (!_streamed->withSound) {
3023 		options.mode = Streaming::Mode::Video;
3024 		options.loop = true;
3025 	} else {
3026 		Assert(_document != nullptr);
3027 		const auto messageId = _message ? _message->fullId() : FullMsgId();
3028 		options.audioId = AudioMsgId(_document, messageId);
3029 		options.speed = Core::App().settings().videoPlaybackSpeed();
3030 		if (_pip) {
3031 			_pip = nullptr;
3032 		}
3033 	}
3034 	_streamed->instance.play(options);
3035 	if (_streamingStartPaused) {
3036 		_streamed->instance.pause();
3037 	} else {
3038 		playbackPauseMusic();
3039 	}
3040 	_streamed->pausedBySeek = false;
3041 
3042 	updatePlaybackState();
3043 }
3044 
playbackControlsSeekProgress(crl::time position)3045 void OverlayWidget::playbackControlsSeekProgress(crl::time position) {
3046 	Expects(_streamed != nullptr);
3047 
3048 	if (!_streamed->instance.player().paused()
3049 		&& !_streamed->instance.player().finished()) {
3050 		_streamed->pausedBySeek = true;
3051 		playbackControlsPause();
3052 	}
3053 }
3054 
playbackControlsSeekFinished(crl::time position)3055 void OverlayWidget::playbackControlsSeekFinished(crl::time position) {
3056 	Expects(_streamed != nullptr);
3057 
3058 	_streamingStartPaused = !_streamed->pausedBySeek
3059 		&& !_streamed->instance.player().finished();
3060 	restartAtSeekPosition(position);
3061 }
3062 
playbackControlsVolumeChanged(float64 volume)3063 void OverlayWidget::playbackControlsVolumeChanged(float64 volume) {
3064 	if (_streamed) {
3065 		Player::mixer()->setVideoVolume(volume);
3066 	}
3067 	Core::App().settings().setVideoVolume(volume);
3068 	Core::App().saveSettingsDelayed();
3069 }
3070 
playbackControlsCurrentVolume()3071 float64 OverlayWidget::playbackControlsCurrentVolume() {
3072 	return Core::App().settings().videoVolume();
3073 }
3074 
playbackControlsVolumeToggled()3075 void OverlayWidget::playbackControlsVolumeToggled() {
3076 	const auto volume = Core::App().settings().videoVolume();
3077 	playbackControlsVolumeChanged(volume ? 0. : _lastPositiveVolume);
3078 }
3079 
playbackControlsVolumeChangeFinished()3080 void OverlayWidget::playbackControlsVolumeChangeFinished() {
3081 	const auto volume = Core::App().settings().videoVolume();
3082 	if (volume > 0.) {
3083 		_lastPositiveVolume = volume;
3084 	}
3085 }
3086 
playbackControlsSpeedChanged(float64 speed)3087 void OverlayWidget::playbackControlsSpeedChanged(float64 speed) {
3088 	DEBUG_LOG(("Media playback speed: change to %1.").arg(speed));
3089 	if (_document) {
3090 		DEBUG_LOG(("Media playback speed: %1 to settings.").arg(speed));
3091 		Core::App().settings().setVideoPlaybackSpeed(speed);
3092 		Core::App().saveSettingsDelayed();
3093 	}
3094 	if (_streamed && !videoIsGifOrUserpic()) {
3095 		DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed));
3096 		_streamed->instance.setSpeed(speed);
3097 	}
3098 }
3099 
playbackControlsCurrentSpeed()3100 float64 OverlayWidget::playbackControlsCurrentSpeed() {
3101 	const auto result = Core::App().settings().videoPlaybackSpeed();
3102 	DEBUG_LOG(("Media playback speed: now %1.").arg(result));
3103 	return result;
3104 }
3105 
switchToPip()3106 void OverlayWidget::switchToPip() {
3107 	Expects(_streamed != nullptr);
3108 	Expects(_document != nullptr);
3109 
3110 	const auto document = _document;
3111 	const auto message = _message;
3112 	const auto closeAndContinue = [=] {
3113 		_showAsPip = false;
3114 		show(OpenRequest(
3115 			findWindow(false),
3116 			document,
3117 			message,
3118 			true));
3119 	};
3120 	_showAsPip = true;
3121 	_pip = std::make_unique<PipWrap>(
3122 		_widget,
3123 		document,
3124 		message ? message->fullId() : FullMsgId(),
3125 		_streamed->instance.shared(),
3126 		closeAndContinue,
3127 		[=] { _pip = nullptr; });
3128 	if (isHidden()) {
3129 		clearBeforeHide();
3130 		clearAfterHide();
3131 	} else {
3132 		close();
3133 		if (const auto window = Core::App().activeWindow()) {
3134 			window->activate();
3135 		}
3136 	}
3137 }
3138 
playbackToggleFullScreen()3139 void OverlayWidget::playbackToggleFullScreen() {
3140 	Expects(_streamed != nullptr);
3141 
3142 	if (!videoShown() || (videoIsGifOrUserpic() && !_fullScreenVideo)) {
3143 		return;
3144 	}
3145 	_fullScreenVideo = !_fullScreenVideo;
3146 	if (_fullScreenVideo) {
3147 		_fullScreenZoomCache = _zoom;
3148 		setZoomLevel(kZoomToScreenLevel, true);
3149 	} else {
3150 		setZoomLevel(_fullScreenZoomCache, true);
3151 		_streamed->controls.showAnimated();
3152 	}
3153 
3154 	_streamed->controls.setInFullScreen(_fullScreenVideo);
3155 	_touchbarFullscreenToggled.fire_copy(_fullScreenVideo);
3156 	updateControls();
3157 	update();
3158 }
3159 
playbackPauseOnCall()3160 void OverlayWidget::playbackPauseOnCall() {
3161 	Expects(_streamed != nullptr);
3162 
3163 	if (_streamed->instance.player().finished()
3164 		|| _streamed->instance.player().paused()) {
3165 		return;
3166 	}
3167 	_streamed->resumeOnCallEnd = true;
3168 	_streamed->instance.pause();
3169 	updatePlaybackState();
3170 }
3171 
playbackResumeOnCall()3172 void OverlayWidget::playbackResumeOnCall() {
3173 	Expects(_streamed != nullptr);
3174 
3175 	if (_streamed->resumeOnCallEnd) {
3176 		_streamed->resumeOnCallEnd = false;
3177 		_streamed->instance.resume();
3178 		updatePlaybackState();
3179 		playbackPauseMusic();
3180 	}
3181 }
3182 
playbackPauseMusic()3183 void OverlayWidget::playbackPauseMusic() {
3184 	Expects(_streamed != nullptr);
3185 
3186 	if (!_streamed->withSound) {
3187 		return;
3188 	}
3189 	Player::instance()->pause(AudioMsgId::Type::Voice);
3190 	Player::instance()->pause(AudioMsgId::Type::Song);
3191 }
3192 
updatePlaybackState()3193 void OverlayWidget::updatePlaybackState() {
3194 	Expects(_streamed != nullptr);
3195 
3196 	if (videoIsGifOrUserpic()) {
3197 		return;
3198 	}
3199 	const auto state = _streamed->instance.player().prepareLegacyState();
3200 	if (state.position != kTimeUnknown && state.length != kTimeUnknown) {
3201 		_streamed->controls.updatePlayback(state);
3202 		_touchbarTrackState.fire_copy(state);
3203 	}
3204 }
3205 
validatePhotoImage(Image * image,bool blurred)3206 void OverlayWidget::validatePhotoImage(Image *image, bool blurred) {
3207 	if (!image) {
3208 		return;
3209 	} else if (!_staticContent.isNull() && (blurred || !_blurred)) {
3210 		return;
3211 	}
3212 	const auto use = flipSizeByRotation({ _width, _height })
3213 		* cIntRetinaFactor();
3214 	setStaticContent(image->pixNoCache(
3215 		use.width(),
3216 		use.height(),
3217 		Images::Option::Smooth
3218 		| (blurred ? Images::Option::Blurred : Images::Option(0))
3219 	).toImage());
3220 	_blurred = blurred;
3221 }
3222 
validatePhotoCurrentImage()3223 void OverlayWidget::validatePhotoCurrentImage() {
3224 	if (!_photo) {
3225 		return;
3226 	}
3227 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Large), false);
3228 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Thumbnail), true);
3229 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Small), true);
3230 	validatePhotoImage(_photoMedia->thumbnailInline(), true);
3231 	if (_staticContent.isNull()
3232 		&& !_message
3233 		&& _peer
3234 		&& _peer->hasUserpic()) {
3235 		if (const auto view = _peer->activeUserpicView()) {
3236 			validatePhotoImage(view->image(), true);
3237 		}
3238 	}
3239 	if (_staticContent.isNull()) {
3240 		_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());
3241 	}
3242 }
3243 
chooseRenderer(Ui::GL::Capabilities capabilities)3244 Ui::GL::ChosenRenderer OverlayWidget::chooseRenderer(
3245 		Ui::GL::Capabilities capabilities) {
3246 	const auto use = Platform::IsMac()
3247 		? true
3248 		: capabilities.transparency;
3249 	LOG(("OpenGL: %1 (OverlayWidget)").arg(Logs::b(use)));
3250 	if (use) {
3251 		_opengl = true;
3252 		return {
3253 			.renderer = std::make_unique<RendererGL>(this),
3254 			.backend = Ui::GL::Backend::OpenGL,
3255 		};
3256 	}
3257 	return {
3258 		.renderer = std::make_unique<RendererSW>(this),
3259 		.backend = Ui::GL::Backend::Raster,
3260 	};
3261 }
3262 
paint(not_null<Renderer * > renderer)3263 void OverlayWidget::paint(not_null<Renderer*> renderer) {
3264 	renderer->paintBackground();
3265 	if (contentShown()) {
3266 		if (videoShown()) {
3267 			renderer->paintTransformedVideoFrame(contentGeometry());
3268 			if (_streamed->instance.player().ready()) {
3269 				_streamed->instance.markFrameShown();
3270 			}
3271 		} else {
3272 			validatePhotoCurrentImage();
3273 			const auto fillTransparentBackground = (!_document
3274 				|| (!_document->sticker() && !_document->isVideoMessage()))
3275 				&& _staticContentTransparent;
3276 			renderer->paintTransformedStaticContent(
3277 				_staticContent,
3278 				contentGeometry(),
3279 				_staticContentTransparent,
3280 				fillTransparentBackground);
3281 		}
3282 		paintRadialLoading(renderer);
3283 	} else {
3284 		if (_themePreviewShown) {
3285 			renderer->paintThemePreview(_themePreviewRect);
3286 		} else if (documentBubbleShown() && !_docRect.isEmpty()) {
3287 			renderer->paintDocumentBubble(_docRect, _docIconRect);
3288 		}
3289 	}
3290 	updateSaveMsgState();
3291 	if (_saveMsgStarted && _saveMsgOpacity.current() > 0.) {
3292 		renderer->paintSaveMsg(_saveMsg);
3293 	}
3294 
3295 	const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current();
3296 	if (opacity > 0) {
3297 		paintControls(renderer, opacity);
3298 		renderer->paintFooter(footerGeometry(), opacity);
3299 		if (!_caption.isEmpty()) {
3300 			renderer->paintCaption(captionGeometry(), opacity);
3301 		}
3302 		if (_groupThumbs) {
3303 			renderer->paintGroupThumbs(
3304 				QRect(
3305 					_groupThumbsLeft,
3306 					_groupThumbsTop,
3307 					width() - 2 * _groupThumbsLeft,
3308 					_groupThumbs->height()),
3309 				opacity);
3310 		}
3311 	}
3312 	checkGroupThumbsAnimation();
3313 }
3314 
checkGroupThumbsAnimation()3315 void OverlayWidget::checkGroupThumbsAnimation() {
3316 	if (_groupThumbs
3317 		&& (!_streamed || _streamed->instance.player().ready())) {
3318 		_groupThumbs->checkForAnimationStart();
3319 	}
3320 }
3321 
paintRadialLoading(not_null<Renderer * > renderer)3322 void OverlayWidget::paintRadialLoading(not_null<Renderer*> renderer) {
3323 	const auto radial = _radial.animating();
3324 	if (_streamed) {
3325 		if (!_streamed->instance.waitingShown()) {
3326 			return;
3327 		}
3328 	} else if (!radial && (!_document || _documentMedia->loaded())) {
3329 		return;
3330 	}
3331 
3332 	const auto radialOpacity = radial ? _radial.opacity() : 0.;
3333 	const auto inner = radialRect();
3334 	Assert(!inner.isEmpty());
3335 
3336 	renderer->paintRadialLoading(inner, radial, radialOpacity);
3337 }
3338 
paintRadialLoadingContent(Painter & p,QRect inner,bool radial,float64 radialOpacity) const3339 void OverlayWidget::paintRadialLoadingContent(
3340 		Painter &p,
3341 		QRect inner,
3342 		bool radial,
3343 		float64 radialOpacity) const {
3344 	const auto arc = inner.marginsRemoved(QMargins(
3345 		st::radialLine,
3346 		st::radialLine,
3347 		st::radialLine,
3348 		st::radialLine));
3349 	const auto paintBg = [&](float64 opacity, QBrush brush) {
3350 		p.setOpacity(opacity);
3351 		p.setPen(Qt::NoPen);
3352 		p.setBrush(brush);
3353 		{
3354 			PainterHighQualityEnabler hq(p);
3355 			p.drawEllipse(inner);
3356 		}
3357 		p.setOpacity(1.);
3358 	};
3359 
3360 	if (_streamed) {
3361 		paintBg(
3362 			_streamed->instance.waitingOpacity(),
3363 			st::radialBg);
3364 		Ui::InfiniteRadialAnimation::Draw(
3365 			p,
3366 			_streamed->instance.waitingState(),
3367 			arc.topLeft(),
3368 			arc.size(),
3369 			width(),
3370 			st::radialFg,
3371 			st::radialLine);
3372 		return;
3373 	}
3374 	if (_photo) {
3375 		paintBg(radialOpacity, st::radialBg);
3376 	} else {
3377 		const auto o = overLevel(OverIcon);
3378 		paintBg(
3379 			_documentMedia->loaded() ? radialOpacity : 1.,
3380 			anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, o));
3381 
3382 		const auto icon = [&]() -> const style::icon * {
3383 			if (radial || _document->loading()) {
3384 				return &st::historyFileThumbCancel;
3385 			}
3386 			return &st::historyFileThumbDownload;
3387 		}();
3388 		if (icon) {
3389 			icon->paintInCenter(p, inner);
3390 		}
3391 	}
3392 	if (radial) {
3393 		p.setOpacity(1);
3394 		_radial.draw(p, arc, st::radialLine, st::radialFg);
3395 	}
3396 }
3397 
paintThemePreviewContent(Painter & p,QRect outer,QRect clip)3398 void OverlayWidget::paintThemePreviewContent(
3399 		Painter &p,
3400 		QRect outer,
3401 		QRect clip) {
3402 	const auto fill = outer.intersected(clip);
3403 	if (!fill.isEmpty()) {
3404 		if (_themePreview) {
3405 			p.drawImage(
3406 				outer.topLeft(),
3407 				_themePreview->preview);
3408 		} else {
3409 			p.fillRect(fill, st::themePreviewBg);
3410 			p.setFont(st::themePreviewLoadingFont);
3411 			p.setPen(st::themePreviewLoadingFg);
3412 			p.drawText(
3413 				outer,
3414 				(_themePreviewId
3415 					? tr::lng_theme_preview_generating(tr::now)
3416 					: tr::lng_theme_preview_invalid(tr::now)),
3417 				QTextOption(style::al_center));
3418 		}
3419 	}
3420 
3421 	const auto fillOverlay = [&](QRect fill) {
3422 		const auto clipped = fill.intersected(clip);
3423 		if (!clipped.isEmpty()) {
3424 			p.setOpacity(st::themePreviewOverlayOpacity);
3425 			p.fillRect(clipped, st::themePreviewBg);
3426 			p.setOpacity(1.);
3427 		}
3428 	};
3429 	auto titleRect = QRect(
3430 		outer.x(),
3431 		outer.y(),
3432 		outer.width(),
3433 		st::themePreviewMargin.top());
3434 	if (titleRect.x() < 0) {
3435 		titleRect = QRect(
3436 			0,
3437 			outer.y(),
3438 			width(),
3439 			st::themePreviewMargin.top());
3440 	}
3441 	if (titleRect.y() < 0) {
3442 		titleRect.moveTop(0);
3443 		fillOverlay(titleRect);
3444 	}
3445 	titleRect = titleRect.marginsRemoved(QMargins(
3446 		st::themePreviewMargin.left(),
3447 		st::themePreviewTitleTop,
3448 		st::themePreviewMargin.right(),
3449 		(titleRect.height()
3450 			- st::themePreviewTitleTop
3451 			- st::themePreviewTitleFont->height)));
3452 	if (titleRect.intersects(clip)) {
3453 		p.setFont(st::themePreviewTitleFont);
3454 		p.setPen(st::themePreviewTitleFg);
3455 		const auto title = _themeCloudData.title.isEmpty()
3456 			? tr::lng_theme_preview_title(tr::now)
3457 			: _themeCloudData.title;
3458 		const auto elided = st::themePreviewTitleFont->elided(
3459 			title,
3460 			titleRect.width());
3461 		p.drawTextLeft(titleRect.x(), titleRect.y(), width(), elided);
3462 	}
3463 
3464 	auto buttonsRect = QRect(
3465 		outer.x(),
3466 		outer.y() + outer.height() - st::themePreviewMargin.bottom(),
3467 		outer.width(),
3468 		st::themePreviewMargin.bottom());
3469 	if (buttonsRect.y() + buttonsRect.height() > height()) {
3470 		buttonsRect.moveTop(height() - buttonsRect.height());
3471 		fillOverlay(buttonsRect);
3472 	}
3473 	if (_themeShare && _themeCloudData.usersCount > 0) {
3474 		p.setFont(st::boxTextFont);
3475 		p.setPen(st::windowSubTextFg);
3476 		const auto left = outer.x()
3477 			+ (_themeShare->x() - _themePreviewRect.x())
3478 			+ _themeShare->width()
3479 			- (st::themePreviewCancelButton.width / 2);
3480 		const auto baseline = outer.y()
3481 			+ (_themeShare->y() - _themePreviewRect.y())
3482 			+ st::themePreviewCancelButton.padding.top()
3483 			+ st::themePreviewCancelButton.textTop
3484 			+ st::themePreviewCancelButton.font->ascent;
3485 		p.drawText(
3486 			left,
3487 			baseline,
3488 			tr::lng_theme_preview_users(
3489 				tr::now,
3490 				lt_count,
3491 				_themeCloudData.usersCount));
3492 	}
3493 }
3494 
paintDocumentBubbleContent(Painter & p,QRect outer,QRect icon,QRect clip) const3495 void OverlayWidget::paintDocumentBubbleContent(
3496 		Painter &p,
3497 		QRect outer,
3498 		QRect icon,
3499 		QRect clip) const {
3500 	p.fillRect(outer, st::mediaviewFileBg);
3501 	if (icon.intersects(clip)) {
3502 		if (!_document || !_document->hasThumbnail()) {
3503 			p.fillRect(icon, _docIconColor);
3504 			const auto radial = _radial.animating();
3505 			const auto radialOpacity = radial ? _radial.opacity() : 0.;
3506 			if ((!_document || _documentMedia->loaded()) && (!radial || radialOpacity < 1) && _docIcon) {
3507 				_docIcon->paint(p, icon.x() + (icon.width() - _docIcon->width()), icon.y(), width());
3508 				p.setPen(st::mediaviewFileExtFg);
3509 				p.setFont(st::mediaviewFileExtFont);
3510 				if (!_docExt.isEmpty()) {
3511 					p.drawText(icon.x() + (icon.width() - _docExtWidth) / 2, icon.y() + st::mediaviewFileExtTop + st::mediaviewFileExtFont->ascent, _docExt);
3512 				}
3513 			}
3514 		} else if (const auto thumbnail = _documentMedia->thumbnail()) {
3515 			int32 rf(cIntRetinaFactor());
3516 			p.drawPixmap(icon.topLeft(), thumbnail->pix(_docThumbw), QRect(_docThumbx * rf, _docThumby * rf, st::mediaviewFileIconSize * rf, st::mediaviewFileIconSize * rf));
3517 		}
3518 	}
3519 	if (!icon.contains(clip)) {
3520 		p.setPen(st::mediaviewFileNameFg);
3521 		p.setFont(st::mediaviewFileNameFont);
3522 		p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileNameTop, width(), _docName, _docNameWidth);
3523 
3524 		p.setPen(st::mediaviewFileSizeFg);
3525 		p.setFont(st::mediaviewFont);
3526 		p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileSizeTop, width(), _docSize, _docSizeWidth);
3527 	}
3528 }
3529 
paintSaveMsgContent(Painter & p,QRect outer,QRect clip)3530 void OverlayWidget::paintSaveMsgContent(
3531 		Painter &p,
3532 		QRect outer,
3533 		QRect clip) {
3534 	p.setOpacity(_saveMsgOpacity.current());
3535 	Ui::FillRoundRect(p, outer, st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners);
3536 	st::mediaviewSaveMsgCheck.paint(p, outer.topLeft() + st::mediaviewSaveMsgCheckPos, width());
3537 
3538 	p.setPen(st::mediaviewSaveMsgFg);
3539 	p.setTextPalette(st::mediaviewTextPalette);
3540 	_saveMsgText.draw(p, outer.x() + st::mediaviewSaveMsgPadding.left(), outer.y() + st::mediaviewSaveMsgPadding.top(), outer.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());
3541 	p.restoreTextPalette();
3542 	p.setOpacity(1);
3543 }
3544 
paintControls(not_null<Renderer * > renderer,float64 opacity)3545 void OverlayWidget::paintControls(
3546 		not_null<Renderer*> renderer,
3547 		float64 opacity) {
3548 	struct Control {
3549 		OverState state = OverNone;
3550 		bool visible = false;
3551 		const QRect &outer;
3552 		const QRect &inner;
3553 		const style::icon &icon;
3554 	};
3555 	const QRect kEmpty;
3556 	// When adding / removing controls please update RendererGL.
3557 	const Control controls[] = {
3558 		{
3559 			OverLeftNav,
3560 			_leftNavVisible,
3561 			_leftNav,
3562 			_leftNavIcon,
3563 			st::mediaviewLeft },
3564 		{
3565 			OverRightNav,
3566 			_rightNavVisible,
3567 			_rightNav,
3568 			_rightNavIcon,
3569 			st::mediaviewRight },
3570 		{
3571 			OverClose,
3572 			true,
3573 			_closeNav,
3574 			_closeNavIcon,
3575 			st::mediaviewClose },
3576 		{
3577 			OverSave,
3578 			_saveVisible,
3579 			kEmpty,
3580 			_saveNavIcon,
3581 			st::mediaviewSave },
3582 		{
3583 			OverRotate,
3584 			_rotateVisible,
3585 			kEmpty,
3586 			_rotateNavIcon,
3587 			st::mediaviewRotate },
3588 		{
3589 			OverMore,
3590 			true,
3591 			kEmpty,
3592 			_moreNavIcon,
3593 			st::mediaviewMore },
3594 	};
3595 
3596 	renderer->paintControlsStart();
3597 	for (const auto &control : controls) {
3598 		if (!control.visible) {
3599 			continue;
3600 		}
3601 		const auto bg = overLevel(control.state);
3602 		const auto icon = bg * st::mediaviewIconOverOpacity
3603 			+ (1 - bg) * st::mediaviewIconOpacity;
3604 		renderer->paintControl(
3605 			control.state,
3606 			control.outer,
3607 			bg * opacity,
3608 			control.inner,
3609 			icon * opacity,
3610 			control.icon);
3611 	}
3612 }
3613 
paintFooterContent(Painter & p,QRect outer,QRect clip,float64 opacity)3614 void OverlayWidget::paintFooterContent(
3615 		Painter &p,
3616 		QRect outer,
3617 		QRect clip,
3618 		float64 opacity) {
3619 	p.setPen(st::mediaviewControlFg);
3620 	p.setFont(st::mediaviewThickFont);
3621 
3622 	// header
3623 	const auto shift = outer.topLeft() - _headerNav.topLeft();
3624 	const auto header = _headerNav.translated(shift);
3625 	const auto name = _nameNav.translated(shift);
3626 	const auto date = _dateNav.translated(shift);
3627 	if (header.intersects(clip)) {
3628 		auto o = _headerHasLink ? overLevel(OverHeader) : 0;
3629 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
3630 		p.drawText(header.left(), header.top() + st::mediaviewThickFont->ascent, _headerText);
3631 
3632 		if (o > 0) {
3633 			p.setOpacity(o * opacity);
3634 			p.drawLine(header.left(), header.top() + st::mediaviewThickFont->ascent + 1, header.right(), header.top() + st::mediaviewThickFont->ascent + 1);
3635 		}
3636 	}
3637 
3638 	p.setFont(st::mediaviewFont);
3639 
3640 	// name
3641 	if (_nameNav.isValid() && name.intersects(clip)) {
3642 		float64 o = _from ? overLevel(OverName) : 0.;
3643 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
3644 		_fromNameLabel.drawElided(p, name.left(), name.top(), name.width());
3645 
3646 		if (o > 0) {
3647 			p.setOpacity(o * opacity);
3648 			p.drawLine(name.left(), name.top() + st::mediaviewFont->ascent + 1, name.right(), name.top() + st::mediaviewFont->ascent + 1);
3649 		}
3650 	}
3651 
3652 	// date
3653 	if (date.intersects(clip)) {
3654 		float64 o = overLevel(OverDate);
3655 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
3656 		p.drawText(date.left(), date.top() + st::mediaviewFont->ascent, _dateText);
3657 
3658 		if (o > 0) {
3659 			p.setOpacity(o * opacity);
3660 			p.drawLine(date.left(), date.top() + st::mediaviewFont->ascent + 1, date.right(), date.top() + st::mediaviewFont->ascent + 1);
3661 		}
3662 	}
3663 }
3664 
footerGeometry() const3665 QRect OverlayWidget::footerGeometry() const {
3666 	return _headerNav.united(_nameNav).united(_dateNav);
3667 }
3668 
paintCaptionContent(Painter & p,QRect outer,QRect clip,float64 opacity)3669 void OverlayWidget::paintCaptionContent(
3670 		Painter &p,
3671 		QRect outer,
3672 		QRect clip,
3673 		float64 opacity) {
3674 	const auto inner = outer.marginsRemoved(st::mediaviewCaptionPadding);
3675 	p.setOpacity(opacity);
3676 	p.setBrush(st::mediaviewCaptionBg);
3677 	p.setPen(Qt::NoPen);
3678 	p.drawRoundedRect(outer, st::mediaviewCaptionRadius, st::mediaviewCaptionRadius);
3679 	if (inner.intersects(clip)) {
3680 		p.setTextPalette(st::mediaviewTextPalette);
3681 		p.setPen(st::mediaviewCaptionFg);
3682 		_caption.drawElided(p, inner.x(), inner.y(), inner.width(), inner.height() / st::mediaviewCaptionStyle.font->height);
3683 		p.restoreTextPalette();
3684 	}
3685 }
3686 
captionGeometry() const3687 QRect OverlayWidget::captionGeometry() const {
3688 	return _captionRect.marginsAdded(st::mediaviewCaptionPadding);
3689 }
3690 
paintGroupThumbsContent(Painter & p,QRect outer,QRect clip,float64 opacity)3691 void OverlayWidget::paintGroupThumbsContent(
3692 		Painter &p,
3693 		QRect outer,
3694 		QRect clip,
3695 		float64 opacity) {
3696 	p.setOpacity(opacity);
3697 	_groupThumbs->paint(p, outer.x(), outer.y(), width());
3698 	if (_groupThumbs->hidden()) {
3699 		_groupThumbs = nullptr;
3700 		_groupThumbsRect = QRect();
3701 	}
3702 }
3703 
updateSaveMsgState()3704 void OverlayWidget::updateSaveMsgState() {
3705 	if (!_saveMsgStarted) {
3706 		return;
3707 	}
3708 	float64 dt = float64(crl::now()) - _saveMsgStarted;
3709 	float64 hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown;
3710 	if (dt >= st::mediaviewSaveMsgShowing
3711 		+ st::mediaviewSaveMsgShown
3712 		+ st::mediaviewSaveMsgHiding) {
3713 		_saveMsgStarted = 0;
3714 		return;
3715 	}
3716 	if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) {
3717 		_saveMsgOpacity.start(0);
3718 	}
3719 	float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing);
3720 	_saveMsgOpacity.update(qMin(progress, 1.), anim::linear);
3721 	if (!_blurred) {
3722 		const auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0)
3723 			? int(AnimationTimerDelta)
3724 			: (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt);
3725 		_saveMsgUpdater.callOnce(nextFrame);
3726 	}
3727 }
3728 
handleKeyPress(not_null<QKeyEvent * > e)3729 void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {
3730 	const auto key = e->key();
3731 	const auto modifiers = e->modifiers();
3732 	const auto ctrl = modifiers.testFlag(Qt::ControlModifier);
3733 	if (_streamed) {
3734 		// Ctrl + F for full screen toggle is in eventFilter().
3735 		const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl)
3736 			&& (key == Qt::Key_Enter || key == Qt::Key_Return);
3737 		if (toggleFull) {
3738 			playbackToggleFullScreen();
3739 			return;
3740 		} else if (key == Qt::Key_Space) {
3741 			playbackPauseResume();
3742 			return;
3743 		} else if (_fullScreenVideo) {
3744 			if (key == Qt::Key_Escape) {
3745 				playbackToggleFullScreen();
3746 			} else if (key == Qt::Key_0) {
3747 				activateControls();
3748 				restartAtSeekPosition(0);
3749 			} else if (key >= Qt::Key_1 && key <= Qt::Key_9) {
3750 				activateControls();
3751 				const auto index = int(key - Qt::Key_0);
3752 				restartAtProgress(index / 10.0);
3753 			} else if (key == Qt::Key_Left) {
3754 				activateControls();
3755 				seekRelativeTime(-kSeekTimeMs);
3756 			} else if (key == Qt::Key_Right) {
3757 				activateControls();
3758 				seekRelativeTime(kSeekTimeMs);
3759 			}
3760 
3761 			return;
3762 		}
3763 	}
3764 	if (!_menu && key == Qt::Key_Escape) {
3765 		if (_document && _document->loading() && !_streamed) {
3766 			handleDocumentClick();
3767 		} else {
3768 			close();
3769 		}
3770 	} else if (e == QKeySequence::Save || e == QKeySequence::SaveAs) {
3771 		saveAs();
3772 	} else if (key == Qt::Key_Copy || (key == Qt::Key_C && ctrl)) {
3773 		copyMedia();
3774 	} else if (key == Qt::Key_Enter
3775 		|| key == Qt::Key_Return
3776 		|| key == Qt::Key_Space) {
3777 		if (_streamed) {
3778 			playbackPauseResume();
3779 		} else if (_document
3780 			&& !_document->loading()
3781 			&& (documentBubbleShown() || !_documentMedia->loaded())) {
3782 			handleDocumentClick();
3783 		}
3784 	} else if (key == Qt::Key_Left) {
3785 		if (_controlsHideTimer.isActive()) {
3786 			activateControls();
3787 		}
3788 		moveToNext(-1);
3789 	} else if (key == Qt::Key_Right) {
3790 		if (_controlsHideTimer.isActive()) {
3791 			activateControls();
3792 		}
3793 		moveToNext(1);
3794 	} else if (ctrl) {
3795 		if (key == Qt::Key_Plus
3796 			|| key == Qt::Key_Equal
3797 			|| key == Qt::Key_Asterisk
3798 			|| key == ']') {
3799 			zoomIn();
3800 		} else if (key == Qt::Key_Minus || key == Qt::Key_Underscore) {
3801 			zoomOut();
3802 		} else if (key == Qt::Key_0) {
3803 			zoomReset();
3804 		} else if (key == Qt::Key_I) {
3805 			update();
3806 		}
3807 	}
3808 }
3809 
handleWheelEvent(not_null<QWheelEvent * > e)3810 void OverlayWidget::handleWheelEvent(not_null<QWheelEvent*> e) {
3811 	constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
3812 
3813 	_verticalWheelDelta += e->angleDelta().y();
3814 	while (qAbs(_verticalWheelDelta) >= step) {
3815 		if (_verticalWheelDelta < 0) {
3816 			_verticalWheelDelta += step;
3817 			if (e->modifiers().testFlag(Qt::ControlModifier)) {
3818 				zoomOut();
3819 			} else if (e->source() == Qt::MouseEventNotSynthesized) {
3820 				moveToNext(1);
3821 			}
3822 		} else {
3823 			_verticalWheelDelta -= step;
3824 			if (e->modifiers().testFlag(Qt::ControlModifier)) {
3825 				zoomIn();
3826 			} else if (e->source() == Qt::MouseEventNotSynthesized) {
3827 				moveToNext(-1);
3828 			}
3829 		}
3830 	}
3831 }
3832 
setZoomLevel(int newZoom,bool force)3833 void OverlayWidget::setZoomLevel(int newZoom, bool force) {
3834 	if (!force && _zoom == newZoom) {
3835 		return;
3836 	}
3837 
3838 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
3839 	float64 nx, ny, z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
3840 	const auto contentSize = videoShown()
3841 		? style::ConvertScale(videoSize())
3842 		: QSize(_width, _height);
3843 	_oldGeometry = contentGeometry();
3844 	_geometryAnimation.stop();
3845 
3846 	_w = contentSize.width();
3847 	_h = contentSize.height();
3848 	if (z >= 0) {
3849 		nx = (_x - width() / 2.) / (z + 1);
3850 		ny = (_y - height() / 2.) / (z + 1);
3851 	} else {
3852 		nx = (_x - width() / 2.) * (-z + 1);
3853 		ny = (_y - height() / 2.) * (-z + 1);
3854 	}
3855 	_zoom = newZoom;
3856 	z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
3857 	if (z > 0) {
3858 		_w = qRound(_w * (z + 1));
3859 		_h = qRound(_h * (z + 1));
3860 		_x = qRound(nx * (z + 1) + width() / 2.);
3861 		_y = qRound(ny * (z + 1) + height() / 2.);
3862 	} else {
3863 		_w = qRound(_w / (-z + 1));
3864 		_h = qRound(_h / (-z + 1));
3865 		_x = qRound(nx / (-z + 1) + width() / 2.);
3866 		_y = qRound(ny / (-z + 1) + height() / 2.);
3867 	}
3868 	snapXY();
3869 	if (_opengl) {
3870 		_geometryAnimation.start(
3871 			[=] { update(); },
3872 			0.,
3873 			1.,
3874 			st::widgetFadeDuration/*,
3875 			anim::easeOutCirc*/);
3876 	}
3877 	update();
3878 }
3879 
entityForUserPhotos(int index) const3880 OverlayWidget::Entity OverlayWidget::entityForUserPhotos(int index) const {
3881 	Expects(_userPhotosData.has_value());
3882 	Expects(_session != nullptr);
3883 
3884 	if (index < 0 || index >= _userPhotosData->size()) {
3885 		return { v::null, nullptr };
3886 	}
3887 	const auto id = (*_userPhotosData)[index];
3888 	if (const auto photo = _session->data().photo(id)) {
3889 		return { photo, nullptr };
3890 	}
3891 	return { v::null, nullptr };
3892 }
3893 
entityForSharedMedia(int index) const3894 OverlayWidget::Entity OverlayWidget::entityForSharedMedia(int index) const {
3895 	Expects(_sharedMediaData.has_value());
3896 
3897 	if (index < 0 || index >= _sharedMediaData->size()) {
3898 		return { v::null, nullptr };
3899 	}
3900 	auto value = (*_sharedMediaData)[index];
3901 	if (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {
3902 		// Last peer photo.
3903 		return { *photo, nullptr };
3904 	} else if (const auto itemId = std::get_if<FullMsgId>(&value)) {
3905 		return entityForItemId(*itemId);
3906 	}
3907 	return { v::null, nullptr };
3908 }
3909 
entityForCollage(int index) const3910 OverlayWidget::Entity OverlayWidget::entityForCollage(int index) const {
3911 	Expects(_collageData.has_value());
3912 	Expects(_session != nullptr);
3913 
3914 	const auto &items = _collageData->items;
3915 	if (!_message || index < 0 || index >= items.size()) {
3916 		return { v::null, nullptr };
3917 	}
3918 	if (const auto document = std::get_if<DocumentData*>(&items[index])) {
3919 		return { *document, _message };
3920 	} else if (const auto photo = std::get_if<PhotoData*>(&items[index])) {
3921 		return { *photo, _message };
3922 	}
3923 	return { v::null, nullptr };
3924 }
3925 
entityForItemId(const FullMsgId & itemId) const3926 OverlayWidget::Entity OverlayWidget::entityForItemId(const FullMsgId &itemId) const {
3927 	Expects(_session != nullptr);
3928 
3929 	if (const auto item = _session->data().message(itemId)) {
3930 		if (const auto media = item->media()) {
3931 			if (const auto photo = media->photo()) {
3932 				return { photo, item };
3933 			} else if (const auto document = media->document()) {
3934 				return { document, item };
3935 			}
3936 		}
3937 		return { v::null, item };
3938 	}
3939 	return { v::null, nullptr };
3940 }
3941 
entityByIndex(int index) const3942 OverlayWidget::Entity OverlayWidget::entityByIndex(int index) const {
3943 	if (_sharedMediaData) {
3944 		return entityForSharedMedia(index);
3945 	} else if (_userPhotosData) {
3946 		return entityForUserPhotos(index);
3947 	} else if (_collageData) {
3948 		return entityForCollage(index);
3949 	}
3950 	return { v::null, nullptr };
3951 }
3952 
setContext(std::variant<v::null_t,not_null<HistoryItem * >,not_null<PeerData * >> context)3953 void OverlayWidget::setContext(
3954 	std::variant<
3955 		v::null_t,
3956 		not_null<HistoryItem*>,
3957 		not_null<PeerData*>> context) {
3958 	if (const auto item = std::get_if<not_null<HistoryItem*>>(&context)) {
3959 		_message = (*item);
3960 		_history = _message->history();
3961 		_peer = _history->peer;
3962 	} else if (const auto peer = std::get_if<not_null<PeerData*>>(&context)) {
3963 		_peer = *peer;
3964 		_history = _peer->owner().history(_peer);
3965 		_message = nullptr;
3966 	} else {
3967 		_message = nullptr;
3968 		_history = nullptr;
3969 		_peer = nullptr;
3970 	}
3971 	_migrated = nullptr;
3972 	if (_history) {
3973 		if (_history->peer->migrateFrom()) {
3974 			_migrated = _history->owner().history(_history->peer->migrateFrom());
3975 		} else if (_history->peer->migrateTo()) {
3976 			_migrated = _history;
3977 			_history = _history->owner().history(_history->peer->migrateTo());
3978 		}
3979 	}
3980 	_user = _peer ? _peer->asUser() : nullptr;
3981 }
3982 
setSession(not_null<Main::Session * > session)3983 void OverlayWidget::setSession(not_null<Main::Session*> session) {
3984 	if (_session == session) {
3985 		return;
3986 	}
3987 
3988 	clearSession();
3989 	_session = session;
3990 	_widget->setWindowIcon(Window::CreateIcon(session));
3991 
3992 	session->downloaderTaskFinished(
3993 	) | rpl::start_with_next([=] {
3994 		if (!isHidden()) {
3995 			updateControls();
3996 			checkForSaveLoaded();
3997 		}
3998 	}, _sessionLifetime);
3999 
4000 	base::ObservableViewer(
4001 		session->documentUpdated
4002 	) | rpl::start_with_next([=](DocumentData *document) {
4003 		if (!isHidden()) {
4004 			documentUpdated(document);
4005 		}
4006 	}, _sessionLifetime);
4007 
4008 	session->data().itemIdChanged(
4009 	) | rpl::start_with_next([=](const Data::Session::IdChange &change) {
4010 		changingMsgId(change.item, change.oldId);
4011 	}, _sessionLifetime);
4012 
4013 	session->data().itemRemoved(
4014 	) | rpl::filter([=](not_null<const HistoryItem*> item) {
4015 		return (_message == item);
4016 	}) | rpl::start_with_next([=] {
4017 		close();
4018 		clearSession();
4019 	}, _sessionLifetime);
4020 
4021 	session->account().sessionChanges(
4022 	) | rpl::start_with_next([=] {
4023 		clearSession();
4024 	}, _sessionLifetime);
4025 }
4026 
moveToNext(int delta)4027 bool OverlayWidget::moveToNext(int delta) {
4028 	if (!_index) {
4029 		return false;
4030 	}
4031 	auto newIndex = *_index + delta;
4032 	return moveToEntity(entityByIndex(newIndex), delta);
4033 }
4034 
moveToEntity(const Entity & entity,int preloadDelta)4035 bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) {
4036 	if (v::is_null(entity.data) && !entity.item) {
4037 		return false;
4038 	}
4039 	if (const auto item = entity.item) {
4040 		setContext(item);
4041 	} else if (_peer) {
4042 		setContext(_peer);
4043 	} else {
4044 		setContext(v::null);
4045 	}
4046 	clearStreaming();
4047 	_streamingStartPaused = false;
4048 	if (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {
4049 		displayPhoto(*photo);
4050 	} else if (auto document = std::get_if<not_null<DocumentData*>>(&entity.data)) {
4051 		displayDocument(*document);
4052 	} else {
4053 		displayDocument(nullptr);
4054 	}
4055 	preloadData(preloadDelta);
4056 	return true;
4057 }
4058 
preloadData(int delta)4059 void OverlayWidget::preloadData(int delta) {
4060 	if (!_index) {
4061 		return;
4062 	}
4063 	auto from = *_index + (delta ? -delta : -1);
4064 	auto till = *_index + (delta ? delta * kPreloadCount : 1);
4065 	if (from > till) std::swap(from, till);
4066 
4067 	auto photos = base::flat_set<std::shared_ptr<Data::PhotoMedia>>();
4068 	auto documents = base::flat_set<std::shared_ptr<Data::DocumentMedia>>();
4069 	for (auto index = from; index != till + 1; ++index) {
4070 		auto entity = entityByIndex(index);
4071 		if (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {
4072 			const auto [i, ok] = photos.emplace((*photo)->createMediaView());
4073 			(*i)->wanted(Data::PhotoSize::Small, fileOrigin(entity));
4074 			(*photo)->load(fileOrigin(entity), LoadFromCloudOrLocal, true);
4075 		} else if (auto document = std::get_if<not_null<DocumentData*>>(
4076 				&entity.data)) {
4077 			const auto [i, ok] = documents.emplace(
4078 				(*document)->createMediaView());
4079 			(*i)->thumbnailWanted(fileOrigin(entity));
4080 			if (!(*i)->canBePlayed()) {
4081 				(*i)->automaticLoad(fileOrigin(entity), entity.item);
4082 			}
4083 		}
4084 	}
4085 	_preloadPhotos = std::move(photos);
4086 	_preloadDocuments = std::move(documents);
4087 }
4088 
handleMousePress(QPoint position,Qt::MouseButton button)4089 void OverlayWidget::handleMousePress(
4090 		QPoint position,
4091 		Qt::MouseButton button) {
4092 	updateOver(position);
4093 	if (_menu || !_receiveMouse) {
4094 		return;
4095 	}
4096 
4097 	ClickHandler::pressed();
4098 
4099 	if (button == Qt::LeftButton) {
4100 		_down = OverNone;
4101 		if (!ClickHandler::getPressed()) {
4102 			if (_over == OverLeftNav && moveToNext(-1)) {
4103 				_lastAction = position;
4104 			} else if (_over == OverRightNav && moveToNext(1)) {
4105 				_lastAction = position;
4106 			} else if (_over == OverName
4107 				|| _over == OverDate
4108 				|| _over == OverHeader
4109 				|| _over == OverSave
4110 				|| _over == OverRotate
4111 				|| _over == OverIcon
4112 				|| _over == OverMore
4113 				|| _over == OverClose
4114 				|| _over == OverVideo) {
4115 				_down = _over;
4116 			} else if (!_saveMsg.contains(position) || !_saveMsgStarted) {
4117 				_pressed = true;
4118 				_dragging = 0;
4119 				updateCursor();
4120 				_mStart = position;
4121 				_xStart = _x;
4122 				_yStart = _y;
4123 			}
4124 		}
4125 	} else if (button == Qt::MiddleButton) {
4126 		zoomReset();
4127 	}
4128 	activateControls();
4129 }
4130 
handleDoubleClick(QPoint position,Qt::MouseButton button)4131 bool OverlayWidget::handleDoubleClick(
4132 		QPoint position,
4133 		Qt::MouseButton button) {
4134 	updateOver(position);
4135 
4136 	if (_over != OverVideo || !_streamed || button != Qt::LeftButton) {
4137 		return false;
4138 	}
4139 	playbackToggleFullScreen();
4140 	playbackPauseResume();
4141 	return true;
4142 }
4143 
snapXY()4144 void OverlayWidget::snapXY() {
4145 	int32 xmin = width() - _w, xmax = 0;
4146 	int32 ymin = height() - _h, ymax = 0;
4147 	if (xmin > (width() - _w) / 2) xmin = (width() - _w) / 2;
4148 	if (xmax < (width() - _w) / 2) xmax = (width() - _w) / 2;
4149 	if (ymin > (height() - _h) / 2) ymin = (height() - _h) / 2;
4150 	if (ymax < (height() - _h) / 2) ymax = (height() - _h) / 2;
4151 	if (_x < xmin) _x = xmin;
4152 	if (_x > xmax) _x = xmax;
4153 	if (_y < ymin) _y = ymin;
4154 	if (_y > ymax) _y = ymax;
4155 }
4156 
handleMouseMove(QPoint position)4157 void OverlayWidget::handleMouseMove(QPoint position) {
4158 	updateOver(position);
4159 	if (_lastAction.x() >= 0
4160 		&& ((position - _lastAction).manhattanLength()
4161 			>= st::mediaviewDeltaFromLastAction)) {
4162 		_lastAction = QPoint(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction);
4163 	}
4164 	if (_pressed) {
4165 		if (!_dragging
4166 			&& ((position - _mStart).manhattanLength()
4167 				>= QApplication::startDragDistance())) {
4168 			_dragging = QRect(_x, _y, _w, _h).contains(_mStart) ? 1 : -1;
4169 			if (_dragging > 0) {
4170 				if (_w > width() || _h > height()) {
4171 					setCursor(style::cur_sizeall);
4172 				} else {
4173 					setCursor(style::cur_default);
4174 				}
4175 			}
4176 		}
4177 		if (_dragging > 0) {
4178 			_x = _xStart + (position - _mStart).x();
4179 			_y = _yStart + (position - _mStart).y();
4180 			snapXY();
4181 			update();
4182 		}
4183 	}
4184 }
4185 
updateOverRect(OverState state)4186 void OverlayWidget::updateOverRect(OverState state) {
4187 	switch (state) {
4188 	case OverLeftNav: update(_leftNav); break;
4189 	case OverRightNav: update(_rightNav); break;
4190 	case OverName: update(_nameNav); break;
4191 	case OverDate: update(_dateNav); break;
4192 	case OverSave: update(_saveNavIcon); break;
4193 	case OverRotate: update(_rotateNavIcon); break;
4194 	case OverIcon: update(_docIconRect); break;
4195 	case OverHeader: update(_headerNav); break;
4196 	case OverClose: update(_closeNav); break;
4197 	case OverMore: update(_moreNavIcon); break;
4198 	}
4199 }
4200 
updateOverState(OverState newState)4201 bool OverlayWidget::updateOverState(OverState newState) {
4202 	bool result = true;
4203 	if (_over != newState) {
4204 		if (newState == OverMore && !_ignoringDropdown) {
4205 			_dropdownShowTimer.callOnce(0);
4206 		} else {
4207 			_dropdownShowTimer.cancel();
4208 		}
4209 		updateOverRect(_over);
4210 		updateOverRect(newState);
4211 		if (_over != OverNone) {
4212 			_animations[_over] = crl::now();
4213 			const auto i = _animationOpacities.find(_over);
4214 			if (i != end(_animationOpacities)) {
4215 				i->second.start(0);
4216 			} else {
4217 				_animationOpacities.emplace(_over, anim::value(1, 0));
4218 			}
4219 			if (!_stateAnimation.animating()) {
4220 				_stateAnimation.start();
4221 			}
4222 		} else {
4223 			result = false;
4224 		}
4225 		_over = newState;
4226 		if (newState != OverNone) {
4227 			_animations[_over] = crl::now();
4228 			const auto i = _animationOpacities.find(_over);
4229 			if (i != end(_animationOpacities)) {
4230 				i->second.start(1);
4231 			} else {
4232 				_animationOpacities.emplace(_over, anim::value(0, 1));
4233 			}
4234 			if (!_stateAnimation.animating()) {
4235 				_stateAnimation.start();
4236 			}
4237 		}
4238 		updateCursor();
4239 	}
4240 	return result;
4241 }
4242 
updateOver(QPoint pos)4243 void OverlayWidget::updateOver(QPoint pos) {
4244 	ClickHandlerPtr lnk;
4245 	ClickHandlerHost *lnkhost = nullptr;
4246 	if (_saveMsgStarted && _saveMsg.contains(pos)) {
4247 		auto textState = _saveMsgText.getState(pos - _saveMsg.topLeft() - QPoint(st::mediaviewSaveMsgPadding.left(), st::mediaviewSaveMsgPadding.top()), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());
4248 		lnk = textState.link;
4249 		lnkhost = this;
4250 	} else if (_captionRect.contains(pos)) {
4251 		auto textState = _caption.getState(pos - _captionRect.topLeft(), _captionRect.width());
4252 		lnk = textState.link;
4253 		lnkhost = this;
4254 	} else if (_groupThumbs && _groupThumbsRect.contains(pos)) {
4255 		const auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop);
4256 		lnk = _groupThumbs->getState(point);
4257 		lnkhost = this;
4258 	}
4259 
4260 
4261 	// retina
4262 	if (pos.x() == width()) {
4263 		pos.setX(pos.x() - 1);
4264 	}
4265 	if (pos.y() == height()) {
4266 		pos.setY(pos.y() - 1);
4267 	}
4268 
4269 	ClickHandler::setActive(lnk, lnkhost);
4270 
4271 	if (_pressed || _dragging) return;
4272 
4273 	if (_fullScreenVideo) {
4274 		updateOverState(OverVideo);
4275 	} else if (_leftNavVisible && _leftNav.contains(pos)) {
4276 		updateOverState(OverLeftNav);
4277 	} else if (_rightNavVisible && _rightNav.contains(pos)) {
4278 		updateOverState(OverRightNav);
4279 	} else if (_from && _nameNav.contains(pos)) {
4280 		updateOverState(OverName);
4281 	} else if (_message && _message->isRegular() && _dateNav.contains(pos)) {
4282 		updateOverState(OverDate);
4283 	} else if (_headerHasLink && _headerNav.contains(pos)) {
4284 		updateOverState(OverHeader);
4285 	} else if (_saveVisible && _saveNav.contains(pos)) {
4286 		updateOverState(OverSave);
4287 	} else if (_rotateVisible && _rotateNav.contains(pos)) {
4288 		updateOverState(OverRotate);
4289 	} else if (_document && documentBubbleShown() && _docIconRect.contains(pos)) {
4290 		updateOverState(OverIcon);
4291 	} else if (_moreNav.contains(pos)) {
4292 		updateOverState(OverMore);
4293 	} else if (_closeNav.contains(pos)) {
4294 		updateOverState(OverClose);
4295 	} else if (documentContentShown() && finalContentRect().contains(pos)) {
4296 		if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) {
4297 			updateOverState(OverVideo);
4298 		} else if (!_streamed && !_documentMedia->loaded()) {
4299 			updateOverState(OverIcon);
4300 		} else if (_over != OverNone) {
4301 			updateOverState(OverNone);
4302 		}
4303 	} else if (_over != OverNone) {
4304 		updateOverState(OverNone);
4305 	}
4306 }
4307 
handleMouseRelease(QPoint position,Qt::MouseButton button)4308 void OverlayWidget::handleMouseRelease(
4309 		QPoint position,
4310 		Qt::MouseButton button) {
4311 	updateOver(position);
4312 
4313 	if (const auto activated = ClickHandler::unpressed()) {
4314 		if (activated->dragText() == qstr("internal:show_saved_message")) {
4315 			showSaveMsgFile();
4316 			return;
4317 		}
4318 		// There may be a mention / hashtag / bot command link.
4319 		// For now activate account for all activated links.
4320 		// findWindow() will activate account.
4321 		ActivateClickHandler(_widget, activated, {
4322 			button,
4323 			QVariant::fromValue(ClickHandlerContext{
4324 				.itemId = _message ? _message->fullId() : FullMsgId(),
4325 				.sessionWindow = base::make_weak(findWindow()),
4326 			})
4327 		});
4328 		return;
4329 	}
4330 
4331 	if (_over == OverName && _down == OverName) {
4332 		if (_from) {
4333 			close();
4334 			Ui::showPeerProfile(_from);
4335 		}
4336 	} else if (_over == OverDate && _down == OverDate) {
4337 		toMessage();
4338 	} else if (_over == OverHeader && _down == OverHeader) {
4339 		showMediaOverview();
4340 	} else if (_over == OverSave && _down == OverSave) {
4341 		downloadMedia();
4342 	} else if (_over == OverRotate && _down == OverRotate) {
4343 		playbackControlsRotate();
4344 	} else if (_over == OverIcon && _down == OverIcon) {
4345 		handleDocumentClick();
4346 	} else if (_over == OverMore && _down == OverMore) {
4347 		InvokeQueued(_widget, [=] { showDropdown(); });
4348 	} else if (_over == OverClose && _down == OverClose) {
4349 		close();
4350 	} else if (_over == OverVideo && _down == OverVideo) {
4351 		if (_streamed) {
4352 			playbackPauseResume();
4353 		}
4354 	} else if (_pressed) {
4355 		if (_dragging) {
4356 			if (_dragging > 0) {
4357 				_x = _xStart + (position - _mStart).x();
4358 				_y = _yStart + (position - _mStart).y();
4359 				snapXY();
4360 				update();
4361 			}
4362 			_dragging = 0;
4363 			setCursor(style::cur_default);
4364 		} else if ((position - _lastAction).manhattanLength()
4365 			>= st::mediaviewDeltaFromLastAction) {
4366 			if (_themePreviewShown) {
4367 				if (!_themePreviewRect.contains(position)) {
4368 					close();
4369 				}
4370 			} else if (!_document
4371 				|| documentContentShown()
4372 				|| !documentBubbleShown()
4373 				|| !_docRect.contains(position)) {
4374 				close();
4375 			}
4376 		}
4377 		_pressed = false;
4378 	}
4379 	_down = OverNone;
4380 	if (!isHidden()) {
4381 		activateControls();
4382 	}
4383 }
4384 
handleContextMenu(std::optional<QPoint> position)4385 bool OverlayWidget::handleContextMenu(std::optional<QPoint> position) {
4386 	if (position && !QRect(_x, _y, _w, _h).contains(*position)) {
4387 		return false;
4388 	}
4389 	_menu = base::make_unique_q<Ui::PopupMenu>(
4390 		_widget,
4391 		st::mediaviewPopupMenu);
4392 	fillContextMenuActions([&] (const QString &text, Fn<void()> handler) {
4393 		_menu->addAction(text, std::move(handler));
4394 	});
4395 	_menu->setDestroyedCallback(crl::guard(_widget, [=] {
4396 		activateControls();
4397 		_receiveMouse = false;
4398 		InvokeQueued(_widget, [=] { receiveMouse(); });
4399 	}));
4400 	_menu->popup(QCursor::pos());
4401 	activateControls();
4402 	return true;
4403 }
4404 
handleTouchEvent(not_null<QTouchEvent * > e)4405 bool OverlayWidget::handleTouchEvent(not_null<QTouchEvent*> e) {
4406 	if (e->device()->type() != base::TouchDevice::TouchScreen) {
4407 		return false;
4408 	} else if (e->type() == QEvent::TouchBegin
4409 		&& !e->touchPoints().isEmpty()
4410 		&& _widget->childAt(
4411 			_widget->mapFromGlobal(
4412 				e->touchPoints().cbegin()->screenPos().toPoint()))) {
4413 		return false;
4414 	}
4415 	switch (e->type()) {
4416 	case QEvent::TouchBegin: {
4417 		if (_touchPress || e->touchPoints().isEmpty()) {
4418 			break;
4419 		}
4420 		_touchTimer.callOnce(QApplication::startDragTime());
4421 		_touchPress = true;
4422 		_touchMove = _touchRightButton = false;
4423 		_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
4424 	} break;
4425 
4426 	case QEvent::TouchUpdate: {
4427 		if (!_touchPress || e->touchPoints().isEmpty()) {
4428 			break;
4429 		}
4430 		if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
4431 			_touchMove = true;
4432 		}
4433 	} break;
4434 
4435 	case QEvent::TouchEnd: {
4436 		if (!_touchPress) {
4437 			break;
4438 		}
4439 		auto weak = Ui::MakeWeak(_widget);
4440 		if (!_touchMove) {
4441 			const auto button = _touchRightButton
4442 				? Qt::RightButton
4443 				: Qt::LeftButton;
4444 			const auto position = _widget->mapFromGlobal(_touchStart);
4445 
4446 			if (weak) handleMousePress(position, button);
4447 			if (weak) handleMouseRelease(position, button);
4448 			if (weak && _touchRightButton) {
4449 				handleContextMenu(position);
4450 			}
4451 		} else if (_touchMove) {
4452 			if ((!_leftNavVisible || !_leftNav.contains(_widget->mapFromGlobal(_touchStart))) && (!_rightNavVisible || !_rightNav.contains(_widget->mapFromGlobal(_touchStart)))) {
4453 				QPoint d = (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart);
4454 				if (d.x() * d.x() > d.y() * d.y() && (d.x() > st::mediaviewSwipeDistance || d.x() < -st::mediaviewSwipeDistance)) {
4455 					moveToNext(d.x() > 0 ? -1 : 1);
4456 				}
4457 			}
4458 		}
4459 		if (weak) {
4460 			_touchTimer.cancel();
4461 			_touchPress = _touchMove = _touchRightButton = false;
4462 		}
4463 	} break;
4464 
4465 	case QEvent::TouchCancel: {
4466 		_touchPress = false;
4467 		_touchTimer.cancel();
4468 	} break;
4469 	}
4470 	return true;
4471 }
4472 
toggleApplicationEventFilter(bool install)4473 void OverlayWidget::toggleApplicationEventFilter(bool install) {
4474 	if (!install) {
4475 		_applicationEventFilter = nullptr;
4476 		return;
4477 	} else if (_applicationEventFilter) {
4478 		return;
4479 	}
4480 	class Filter final : public QObject {
4481 	public:
4482 		explicit Filter(not_null<OverlayWidget*> owner) : _owner(owner) {
4483 		}
4484 
4485 	private:
4486 		bool eventFilter(QObject *obj, QEvent *e) override {
4487 			return obj && e && _owner->filterApplicationEvent(obj, e);
4488 		}
4489 
4490 		const not_null<OverlayWidget*> _owner;
4491 
4492 	};
4493 
4494 	_applicationEventFilter = std::make_unique<Filter>(this);
4495 	qApp->installEventFilter(_applicationEventFilter.get());
4496 }
4497 
filterApplicationEvent(not_null<QObject * > object,not_null<QEvent * > e)4498 bool OverlayWidget::filterApplicationEvent(
4499 		not_null<QObject*> object,
4500 		not_null<QEvent*> e) {
4501 	const auto type = e->type();
4502 	if (type == QEvent::ShortcutOverride) {
4503 		const auto keyEvent = static_cast<QKeyEvent*>(e.get());
4504 		const auto ctrl = keyEvent->modifiers().testFlag(Qt::ControlModifier);
4505 		if (keyEvent->key() == Qt::Key_F && ctrl && _streamed) {
4506 			playbackToggleFullScreen();
4507 		}
4508 		return true;
4509 	} else if (type == QEvent::MouseMove
4510 		|| type == QEvent::MouseButtonPress
4511 		|| type == QEvent::MouseButtonRelease) {
4512 		if (object->isWidgetType()
4513 			&& _widget->isAncestorOf(static_cast<QWidget*>(object.get()))) {
4514 			const auto mouseEvent = static_cast<QMouseEvent*>(e.get());
4515 			const auto mousePosition = _widget->mapFromGlobal(
4516 				mouseEvent->globalPos());
4517 			const auto delta = (mousePosition - _lastMouseMovePos);
4518 			auto activate = delta.manhattanLength()
4519 				>= st::mediaviewDeltaFromLastAction;
4520 			if (activate) {
4521 				_lastMouseMovePos = mousePosition;
4522 			}
4523 			if (type == QEvent::MouseButtonPress) {
4524 				_mousePressed = true;
4525 				activate = true;
4526 			} else if (type == QEvent::MouseButtonRelease) {
4527 				_mousePressed = false;
4528 				activate = true;
4529 			}
4530 			if (activate) {
4531 				activateControls();
4532 			}
4533 		}
4534 	}
4535 	return false;
4536 }
4537 
applyHideWindowWorkaround()4538 void OverlayWidget::applyHideWindowWorkaround() {
4539 	// QOpenGLWidget can't properly destroy a child widget if it is hidden
4540 	// exactly after that, the child is cached in the backing store.
4541 	// So on next paint we force full backing store repaint.
4542 	if (_opengl && !isHidden() && !_hideWorkaround) {
4543 		_hideWorkaround = std::make_unique<Ui::RpWidget>(_widget);
4544 		const auto raw = _hideWorkaround.get();
4545 		raw->setGeometry(_widget->rect());
4546 		raw->show();
4547 		raw->paintRequest(
4548 		) | rpl::start_with_next([=] {
4549 			if (_hideWorkaround.get() == raw) {
4550 				_hideWorkaround.release();
4551 			}
4552 			QPainter(raw).fillRect(raw->rect(), QColor(0, 1, 0, 1));
4553 			crl::on_main(raw, [=] {
4554 				delete raw;
4555 			});
4556 		}, raw->lifetime());
4557 		raw->update();
4558 
4559 		if (Platform::IsWindows()) {
4560 			Ui::Platform::UpdateOverlayed(_widget);
4561 		}
4562 	}
4563 }
4564 
findWindow(bool switchTo) const4565 Window::SessionController *OverlayWidget::findWindow(bool switchTo) const {
4566 	if (!_session) {
4567 		return nullptr;
4568 	}
4569 
4570 	const auto window = _window.get();
4571 	if (window) {
4572 		if (const auto controller = window->sessionController()) {
4573 			if (&controller->session() == _session) {
4574 				return controller;
4575 			}
4576 		}
4577 	}
4578 
4579 	const auto &active = _session->windows();
4580 	if (!active.empty()) {
4581 		return active.front();
4582 	} else if (window && switchTo) {
4583 		Window::SessionController *controllerPtr = nullptr;
4584 		window->invokeForSessionController(
4585 			&_session->account(),
4586 			[&](not_null<Window::SessionController*> newController) {
4587 				controllerPtr = newController;
4588 			});
4589 		return controllerPtr;
4590 	}
4591 
4592 	return nullptr;
4593 }
4594 
4595 // #TODO unite and check
clearBeforeHide()4596 void OverlayWidget::clearBeforeHide() {
4597 	_sharedMedia = nullptr;
4598 	_sharedMediaData = std::nullopt;
4599 	_sharedMediaDataKey = std::nullopt;
4600 	_userPhotos = nullptr;
4601 	_userPhotosData = std::nullopt;
4602 	_collage = nullptr;
4603 	_collageData = std::nullopt;
4604 	assignMediaPointer(nullptr);
4605 	_preloadPhotos.clear();
4606 	_preloadDocuments.clear();
4607 	if (_menu) {
4608 		_menu->hideMenu(true);
4609 	}
4610 	_controlsHideTimer.cancel();
4611 	_controlsState = ControlsShown;
4612 	_controlsOpacity = anim::value(1, 1);
4613 	_groupThumbs = nullptr;
4614 	_groupThumbsRect = QRect();
4615 	for (const auto child : _widget->children()) {
4616 		if (child->isWidgetType() && _hideWorkaround.get() != child) {
4617 			static_cast<QWidget*>(child)->hide();
4618 		}
4619 	}
4620 }
4621 
clearAfterHide()4622 void OverlayWidget::clearAfterHide() {
4623 	clearStreaming();
4624 	destroyThemePreview();
4625 	_radial.stop();
4626 	_staticContent = QImage();
4627 	_themePreview = nullptr;
4628 	_themeApply.destroyDelayed();
4629 	_themeCancel.destroyDelayed();
4630 	_themeShare.destroyDelayed();
4631 }
4632 
receiveMouse()4633 void OverlayWidget::receiveMouse() {
4634 	_receiveMouse = true;
4635 }
4636 
showDropdown()4637 void OverlayWidget::showDropdown() {
4638 	_dropdown->clearActions();
4639 	fillContextMenuActions([&] (const QString &text, Fn<void()> handler) {
4640 		_dropdown->addAction(text, std::move(handler));
4641 	});
4642 	_dropdown->moveToRight(0, height() - _dropdown->height());
4643 	_dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
4644 	_dropdown->setFocus();
4645 }
4646 
handleTouchTimer()4647 void OverlayWidget::handleTouchTimer() {
4648 	_touchRightButton = true;
4649 }
4650 
updateImage()4651 void OverlayWidget::updateImage() {
4652 	update(_saveMsg);
4653 }
4654 
findCurrent()4655 void OverlayWidget::findCurrent() {
4656 	using namespace rpl::mappers;
4657 	if (_sharedMediaData) {
4658 		_index = _message
4659 			? _sharedMediaData->indexOf(_message->fullId())
4660 			: _photo ? _sharedMediaData->indexOf(_photo) : std::nullopt;
4661 		_fullIndex = _sharedMediaData->skippedBefore()
4662 			? (_index | func::add(*_sharedMediaData->skippedBefore()))
4663 			: std::nullopt;
4664 		_fullCount = _sharedMediaData->fullCount();
4665 	} else if (_userPhotosData) {
4666 		_index = _photo ? _userPhotosData->indexOf(_photo->id) : std::nullopt;
4667 		_fullIndex = _userPhotosData->skippedBefore()
4668 			? (_index | func::add(*_userPhotosData->skippedBefore()))
4669 			: std::nullopt;
4670 		_fullCount = _userPhotosData->fullCount();
4671 	} else if (_collageData) {
4672 		const auto item = _photo ? WebPageCollage::Item(_photo) : _document;
4673 		const auto &items = _collageData->items;
4674 		const auto i = ranges::find(items, item);
4675 		_index = (i != end(items))
4676 			? std::make_optional(int(i - begin(items)))
4677 			: std::nullopt;
4678 		_fullIndex = _index;
4679 		_fullCount = items.size();
4680 	} else {
4681 		_index = _fullIndex = _fullCount = std::nullopt;
4682 	}
4683 }
4684 
updateHeader()4685 void OverlayWidget::updateHeader() {
4686 	auto index = _fullIndex ? *_fullIndex : -1;
4687 	auto count = _fullCount ? *_fullCount : -1;
4688 	if (index >= 0 && index < count && count > 1) {
4689 		if (_document) {
4690 			_headerText = tr::lng_mediaview_file_n_of_amount(
4691 				tr::now,
4692 				lt_file,
4693 				(_document->filename().isEmpty()
4694 					? tr::lng_mediaview_doc_image(tr::now)
4695 					: _document->filename()),
4696 				lt_n,
4697 				QString::number(index + 1),
4698 				lt_amount,
4699 				QString::number(count));
4700 		} else {
4701 			_headerText = tr::lng_mediaview_n_of_amount(
4702 				tr::now,
4703 				lt_n,
4704 				QString::number(index + 1),
4705 				lt_amount,
4706 				QString::number(count));
4707 		}
4708 	} else {
4709 		if (_document) {
4710 			_headerText = _document->filename().isEmpty() ? tr::lng_mediaview_doc_image(tr::now) : _document->filename();
4711 		} else if (_message) {
4712 			_headerText = tr::lng_mediaview_single_photo(tr::now);
4713 		} else if (_user) {
4714 			_headerText = tr::lng_mediaview_profile_photo(tr::now);
4715 		} else if ((_history && _history->channelId() && !_history->isMegagroup())
4716 			|| (_peer && _peer->isChannel() && !_peer->isMegagroup())) {
4717 			_headerText = tr::lng_mediaview_channel_photo(tr::now);
4718 		} else if (_peer) {
4719 			_headerText = tr::lng_mediaview_group_photo(tr::now);
4720 		} else {
4721 			_headerText = tr::lng_mediaview_single_photo(tr::now);
4722 		}
4723 	}
4724 	_headerHasLink = computeOverviewType() != std::nullopt;
4725 	auto hwidth = st::mediaviewThickFont->width(_headerText);
4726 	if (hwidth > width() / 3) {
4727 		hwidth = width() / 3;
4728 		_headerText = st::mediaviewThickFont->elided(_headerText, hwidth, Qt::ElideMiddle);
4729 	}
4730 	_headerNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height);
4731 }
4732 
overLevel(OverState control) const4733 float64 OverlayWidget::overLevel(OverState control) const {
4734 	auto i = _animationOpacities.find(control);
4735 	return (i == end(_animationOpacities))
4736 		? (_over == control ? 1. : 0.)
4737 		: i->second.current();
4738 }
4739 
4740 } // namespace View
4741 } // namespace Media
4742