4992 lines
		
	
	
	
		
			136 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			4992 lines
		
	
	
	
		
			136 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "media/view/media_view_overlay_widget.h"
 | |
| 
 | |
| #include "apiwrap.h"
 | |
| #include "api/api_attached_stickers.h"
 | |
| #include "api/api_peer_photo.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "mainwindow.h"
 | |
| #include "core/application.h"
 | |
| #include "core/click_handler_types.h"
 | |
| #include "core/file_utilities.h"
 | |
| #include "core/mime_type.h"
 | |
| #include "core/ui_integration.h"
 | |
| #include "core/crash_reports.h"
 | |
| #include "ui/widgets/popup_menu.h"
 | |
| #include "ui/widgets/buttons.h"
 | |
| #include "ui/image/image.h"
 | |
| #include "ui/text/text_utilities.h"
 | |
| #include "ui/platform/ui_platform_utility.h"
 | |
| #include "ui/toast/toast.h"
 | |
| #include "ui/toasts/common_toasts.h"
 | |
| #include "ui/text/format_values.h"
 | |
| #include "ui/item_text_options.h"
 | |
| #include "ui/painter.h"
 | |
| #include "ui/ui_utility.h"
 | |
| #include "ui/cached_round_corners.h"
 | |
| #include "ui/gl/gl_surface.h"
 | |
| #include "ui/boxes/confirm_box.h"
 | |
| #include "info/info_memento.h"
 | |
| #include "info/info_controller.h"
 | |
| #include "boxes/delete_messages_box.h"
 | |
| #include "boxes/report_messages_box.h"
 | |
| #include "media/audio/media_audio.h"
 | |
| #include "media/view/media_view_playback_controls.h"
 | |
| #include "media/view/media_view_group_thumbs.h"
 | |
| #include "media/view/media_view_pip.h"
 | |
| #include "media/view/media_view_overlay_raster.h"
 | |
| #include "media/view/media_view_overlay_opengl.h"
 | |
| #include "media/streaming/media_streaming_instance.h"
 | |
| #include "media/streaming/media_streaming_player.h"
 | |
| #include "media/player/media_player_instance.h"
 | |
| #include "history/history.h"
 | |
| #include "history/history_item.h"
 | |
| #include "history/history_item_helpers.h"
 | |
| #include "history/view/media/history_view_media.h"
 | |
| #include "data/data_media_types.h"
 | |
| #include "data/data_session.h"
 | |
| #include "data/data_changes.h"
 | |
| #include "data/data_channel.h"
 | |
| #include "data/data_chat.h"
 | |
| #include "data/data_user.h"
 | |
| #include "data/data_file_origin.h"
 | |
| #include "data/data_media_rotation.h"
 | |
| #include "data/data_photo_media.h"
 | |
| #include "data/data_document_media.h"
 | |
| #include "data/data_document_resolver.h"
 | |
| #include "data/data_file_click_handler.h"
 | |
| #include "data/data_download_manager.h"
 | |
| #include "window/themes/window_theme_preview.h"
 | |
| #include "window/window_peer_menu.h"
 | |
| #include "window/window_session_controller.h"
 | |
| #include "window/window_controller.h"
 | |
| #include "base/platform/base_platform_info.h"
 | |
| #include "base/power_save_blocker.h"
 | |
| #include "base/random.h"
 | |
| #include "base/unixtime.h"
 | |
| #include "base/qt_signal_producer.h"
 | |
| #include "base/qt/qt_common_adapters.h"
 | |
| #include "base/event_filter.h"
 | |
| #include "main/main_account.h"
 | |
| #include "main/main_domain.h" // Domain::activeSessionValue.
 | |
| #include "main/main_session.h"
 | |
| #include "main/main_session_settings.h"
 | |
| #include "layout/layout_document_generic_preview.h"
 | |
| #include "storage/file_download.h"
 | |
| #include "storage/storage_account.h"
 | |
| #include "calls/calls_instance.h"
 | |
| #include "styles/style_media_view.h"
 | |
| #include "styles/style_chat.h"
 | |
| #include "styles/style_menu_icons.h"
 | |
| 
 | |
| #ifdef Q_OS_MAC
 | |
| #include "platform/mac/touchbar/mac_touchbar_media_view.h"
 | |
| #endif // Q_OS_MAC
 | |
| 
 | |
| #include <QtWidgets/QApplication>
 | |
| #include <QtCore/QBuffer>
 | |
| #include <QtGui/QGuiApplication>
 | |
| #include <QtGui/QClipboard>
 | |
| #include <QtGui/QWindow>
 | |
| #include <QtGui/QScreen>
 | |
| 
 | |
| namespace Media {
 | |
| namespace View {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kPreloadCount = 3;
 | |
| constexpr auto kMaxZoomLevel = 7; // x8
 | |
| constexpr auto kZoomToScreenLevel = 1024;
 | |
| constexpr auto kOverlayLoaderPriority = 2;
 | |
| constexpr auto kSeekTimeMs = 5 * crl::time(1000);
 | |
| 
 | |
| // macOS OpenGL renderer fails to render larger texture
 | |
| // even though it reports that max texture size is 16384.
 | |
| constexpr auto kMaxDisplayImageSize = 4096;
 | |
| 
 | |
| // Preload X message ids before and after current.
 | |
| constexpr auto kIdsLimit = 48;
 | |
| 
 | |
| // Preload next messages if we went further from current than that.
 | |
| constexpr auto kIdsPreloadAfter = 28;
 | |
| 
 | |
| class PipDelegate final : public Pip::Delegate {
 | |
| public:
 | |
| 	PipDelegate(QWidget *parent, not_null<Main::Session*> session);
 | |
| 
 | |
| 	void pipSaveGeometry(QByteArray geometry) override;
 | |
| 	QByteArray pipLoadGeometry() override;
 | |
| 	float64 pipPlaybackSpeed() override;
 | |
| 	QWidget *pipParentWidget() override;
 | |
| 
 | |
| private:
 | |
| 	QWidget *_parent = nullptr;
 | |
| 	not_null<Main::Session*> _session;
 | |
| 
 | |
| };
 | |
| 
 | |
| PipDelegate::PipDelegate(QWidget *parent, not_null<Main::Session*> session)
 | |
| : _parent(parent)
 | |
| , _session(session) {
 | |
| }
 | |
| 
 | |
| void PipDelegate::pipSaveGeometry(QByteArray geometry) {
 | |
| 	Core::App().settings().setVideoPipGeometry(geometry);
 | |
| 	Core::App().saveSettingsDelayed();
 | |
| }
 | |
| 
 | |
| QByteArray PipDelegate::pipLoadGeometry() {
 | |
| 	return Core::App().settings().videoPipGeometry();
 | |
| }
 | |
| 
 | |
| float64 PipDelegate::pipPlaybackSpeed() {
 | |
| 	return Core::App().settings().videoPlaybackSpeed();
 | |
| }
 | |
| 
 | |
| QWidget *PipDelegate::pipParentWidget() {
 | |
| 	return _parent;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] Images::Options VideoThumbOptions(DocumentData *document) {
 | |
| 	const auto result = Images::Option::Blur;
 | |
| 	return (document && document->isVideoMessage())
 | |
| 		? (result | Images::Option::RoundCircle)
 | |
| 		: result;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] QImage PrepareStaticImage(Images::ReadArgs &&args) {
 | |
| 	auto read = Images::Read(std::move(args));
 | |
| 	return (read.image.width() > kMaxDisplayImageSize
 | |
| 		|| read.image.height() > kMaxDisplayImageSize)
 | |
| 		? read.image.scaled(
 | |
| 			kMaxDisplayImageSize,
 | |
| 			kMaxDisplayImageSize,
 | |
| 			Qt::KeepAspectRatio,
 | |
| 			Qt::SmoothTransformation)
 | |
| 		: read.image;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] bool IsSemitransparent(const QImage &image) {
 | |
| 	if (image.isNull()) {
 | |
| 		return true;
 | |
| 	} else if (!image.hasAlphaChannel()) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	Assert(image.format() == QImage::Format_ARGB32_Premultiplied);
 | |
| 	constexpr auto kAlphaMask = 0xFF000000;
 | |
| 	auto ints = reinterpret_cast<const uint32*>(image.bits());
 | |
| 	const auto add = (image.bytesPerLine() / 4) - image.width();
 | |
| 	for (auto y = 0; y != image.height(); ++y) {
 | |
| 		for (auto till = ints + image.width(); ints != till; ++ints) {
 | |
| 			if ((*ints & kAlphaMask) != kAlphaMask) {
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 		ints += add;
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct OverlayWidget::SharedMedia {
 | |
| 	SharedMedia(SharedMediaKey key) : key(key) {
 | |
| 	}
 | |
| 
 | |
| 	SharedMediaKey key;
 | |
| 	rpl::lifetime lifetime;
 | |
| };
 | |
| 
 | |
| struct OverlayWidget::UserPhotos {
 | |
| 	UserPhotos(UserPhotosKey key) : key(key) {
 | |
| 	}
 | |
| 
 | |
| 	UserPhotosKey key;
 | |
| 	rpl::lifetime lifetime;
 | |
| };
 | |
| 
 | |
| struct OverlayWidget::Collage {
 | |
| 	Collage(CollageKey key) : key(key) {
 | |
| 	}
 | |
| 
 | |
| 	CollageKey key;
 | |
| };
 | |
| 
 | |
| struct OverlayWidget::Streamed {
 | |
| 	Streamed(
 | |
| 		not_null<DocumentData*> document,
 | |
| 		Data::FileOrigin origin,
 | |
| 		not_null<QWidget*> controlsParent,
 | |
| 		not_null<PlaybackControls::Delegate*> controlsDelegate,
 | |
| 		Fn<void()> waitingCallback);
 | |
| 	Streamed(
 | |
| 		not_null<PhotoData*> photo,
 | |
| 		Data::FileOrigin origin,
 | |
| 		not_null<QWidget*> controlsParent,
 | |
| 		not_null<PlaybackControls::Delegate*> controlsDelegate,
 | |
| 		Fn<void()> waitingCallback);
 | |
| 
 | |
| 	Streaming::Instance instance;
 | |
| 	PlaybackControls controls;
 | |
| 	std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
 | |
| 
 | |
| 	bool withSound = false;
 | |
| 	bool pausedBySeek = false;
 | |
| 	bool resumeOnCallEnd = false;
 | |
| };
 | |
| 
 | |
| struct OverlayWidget::PipWrap {
 | |
| 	PipWrap(
 | |
| 		QWidget *parent,
 | |
| 		not_null<DocumentData*> document,
 | |
| 		std::shared_ptr<Streaming::Document> shared,
 | |
| 		FnMut<void()> closeAndContinue,
 | |
| 		FnMut<void()> destroy);
 | |
| 
 | |
| 	PipWrap(const PipWrap &other) = delete;
 | |
| 	PipWrap &operator=(const PipWrap &other) = delete;
 | |
| 
 | |
| 	PipDelegate delegate;
 | |
| 	Pip wrapped;
 | |
| 	rpl::lifetime lifetime;
 | |
| };
 | |
| 
 | |
| OverlayWidget::Streamed::Streamed(
 | |
| 	not_null<DocumentData*> document,
 | |
| 	Data::FileOrigin origin,
 | |
| 	not_null<QWidget*> controlsParent,
 | |
| 	not_null<PlaybackControls::Delegate*> controlsDelegate,
 | |
| 	Fn<void()> waitingCallback)
 | |
| : instance(document, origin, std::move(waitingCallback))
 | |
| , controls(controlsParent, controlsDelegate) {
 | |
| }
 | |
| 
 | |
| OverlayWidget::Streamed::Streamed(
 | |
| 	not_null<PhotoData*> photo,
 | |
| 	Data::FileOrigin origin,
 | |
| 	not_null<QWidget*> controlsParent,
 | |
| 	not_null<PlaybackControls::Delegate*> controlsDelegate,
 | |
| 	Fn<void()> waitingCallback)
 | |
| : instance(photo, origin, std::move(waitingCallback))
 | |
| , controls(controlsParent, controlsDelegate) {
 | |
| }
 | |
| 
 | |
| OverlayWidget::PipWrap::PipWrap(
 | |
| 	QWidget *parent,
 | |
| 	not_null<DocumentData*> document,
 | |
| 	std::shared_ptr<Streaming::Document> shared,
 | |
| 	FnMut<void()> closeAndContinue,
 | |
| 	FnMut<void()> destroy)
 | |
| : delegate(parent, &document->session())
 | |
| , wrapped(
 | |
| 	&delegate,
 | |
| 	document,
 | |
| 	std::move(shared),
 | |
| 	std::move(closeAndContinue),
 | |
| 	std::move(destroy)) {
 | |
| }
 | |
| 
 | |
| OverlayWidget::OverlayWidget()
 | |
| : _surface(Ui::GL::CreateSurface(
 | |
| 	[=](Ui::GL::Capabilities capabilities) {
 | |
| 		return chooseRenderer(capabilities);
 | |
| 	}))
 | |
| , _widget(_surface->rpWidget())
 | |
| , _docDownload(_widget, tr::lng_media_download(tr::now), st::mediaviewFileLink)
 | |
| , _docSaveAs(_widget, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink)
 | |
| , _docCancel(_widget, tr::lng_cancel(tr::now), st::mediaviewFileLink)
 | |
| , _radial([=](crl::time now) { return radialAnimationCallback(now); })
 | |
| , _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction)
 | |
| , _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); })
 | |
| , _dropdown(_widget, st::mediaviewDropdownMenu) {
 | |
| 	CrashReports::SetAnnotation("OpenGL Renderer", "[not-initialized]");
 | |
| 
 | |
| 	Lang::Updated(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		refreshLang();
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	_lastPositiveVolume = (Core::App().settings().videoVolume() > 0.)
 | |
| 		? Core::App().settings().videoVolume()
 | |
| 		: Core::Settings::kDefaultVolume;
 | |
| 
 | |
| 	_widget->setWindowTitle(u"Media viewer"_q);
 | |
| 
 | |
| 	const auto text = tr::lng_mediaview_saved_to(
 | |
| 		tr::now,
 | |
| 		lt_downloads,
 | |
| 		Ui::Text::Link(
 | |
| 			tr::lng_mediaview_downloads(tr::now),
 | |
| 			"internal:show_saved_message"),
 | |
| 		Ui::Text::WithEntities);
 | |
| 	_saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text);
 | |
| 	_saveMsg = QRect(0, 0, _saveMsgText.maxWidth() + st::mediaviewSaveMsgPadding.left() + st::mediaviewSaveMsgPadding.right(), st::mediaviewSaveMsgStyle.font->height + st::mediaviewSaveMsgPadding.top() + st::mediaviewSaveMsgPadding.bottom());
 | |
| 	_saveMsgImage = QImage(
 | |
| 		_saveMsg.size() * cIntRetinaFactor(),
 | |
| 		QImage::Format_ARGB32_Premultiplied);
 | |
| 	_saveMsgTimer.setCallback([=, delay = st::mediaviewSaveMsgHiding] {
 | |
| 		_saveMsgAnimation.start([=] { updateImage(); }, 1., 0., delay);
 | |
| 	});
 | |
| 
 | |
| 	_docRectImage = QImage(
 | |
| 		st::mediaviewFileSize * cIntRetinaFactor(),
 | |
| 		QImage::Format_ARGB32_Premultiplied);
 | |
| 	_docRectImage.setDevicePixelRatio(cIntRetinaFactor());
 | |
| 
 | |
| 	_surface->shownValue(
 | |
| 	) | rpl::start_with_next([=](bool shown) {
 | |
| 		toggleApplicationEventFilter(shown);
 | |
| 		if (shown) {
 | |
| 			const auto geometry = _widget->geometry();
 | |
| 			const auto screenList = QGuiApplication::screens();
 | |
| 			DEBUG_LOG(("Viewer Pos: Shown, geometry: %1, %2, %3, %4, screen number: %5")
 | |
| 				.arg(geometry.x())
 | |
| 				.arg(geometry.y())
 | |
| 				.arg(geometry.width())
 | |
| 				.arg(geometry.height())
 | |
| 				.arg(screenList.indexOf(_widget->screen())));
 | |
| 			moveToScreen();
 | |
| 		} else {
 | |
| 			clearAfterHide();
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	const auto mousePosition = [](not_null<QEvent*> e) {
 | |
| 		return static_cast<QMouseEvent*>(e.get())->pos();
 | |
| 	};
 | |
| 	const auto mouseButton = [](not_null<QEvent*> e) {
 | |
| 		return static_cast<QMouseEvent*>(e.get())->button();
 | |
| 	};
 | |
| 	base::install_event_filter(_widget, [=](not_null<QEvent*> e) {
 | |
| 		const auto type = e->type();
 | |
| 		if (type == QEvent::Move) {
 | |
| 			const auto position = static_cast<QMoveEvent*>(e.get())->pos();
 | |
| 			DEBUG_LOG(("Viewer Pos: Moved to %1, %2")
 | |
| 				.arg(position.x())
 | |
| 				.arg(position.y()));
 | |
| 			moveToScreen(true);
 | |
| 		} else if (type == QEvent::Resize) {
 | |
| 			const auto size = static_cast<QResizeEvent*>(e.get())->size();
 | |
| 			DEBUG_LOG(("Viewer Pos: Resized to %1, %2")
 | |
| 				.arg(size.width())
 | |
| 				.arg(size.height()));
 | |
| 			updateControlsGeometry();
 | |
| 		} else if (type == QEvent::MouseButtonPress) {
 | |
| 			handleMousePress(mousePosition(e), mouseButton(e));
 | |
| 		} else if (type == QEvent::MouseButtonRelease) {
 | |
| 			handleMouseRelease(mousePosition(e), mouseButton(e));
 | |
| 		} else if (type == QEvent::MouseMove) {
 | |
| 			handleMouseMove(mousePosition(e));
 | |
| 		} else if (type == QEvent::KeyPress) {
 | |
| 			handleKeyPress(static_cast<QKeyEvent*>(e.get()));
 | |
| 		} else if (type == QEvent::ContextMenu) {
 | |
| 			const auto event = static_cast<QContextMenuEvent*>(e.get());
 | |
| 			const auto mouse = (event->reason() == QContextMenuEvent::Mouse);
 | |
| 			const auto position = mouse
 | |
| 				? std::make_optional(event->pos())
 | |
| 				: std::nullopt;
 | |
| 			if (handleContextMenu(position)) {
 | |
| 				return base::EventFilterResult::Cancel;
 | |
| 			}
 | |
| 		} else if (type == QEvent::MouseButtonDblClick) {
 | |
| 			if (handleDoubleClick(mousePosition(e), mouseButton(e))) {
 | |
| 				return base::EventFilterResult::Cancel;
 | |
| 			} else {
 | |
| 				handleMousePress(mousePosition(e), mouseButton(e));
 | |
| 			}
 | |
| 		} else if (type == QEvent::TouchBegin
 | |
| 			|| type == QEvent::TouchUpdate
 | |
| 			|| type == QEvent::TouchEnd
 | |
| 			|| type == QEvent::TouchCancel) {
 | |
| 			if (handleTouchEvent(static_cast<QTouchEvent*>(e.get()))) {
 | |
| 				return base::EventFilterResult::Cancel;;
 | |
| 			}
 | |
| 		} else if (type == QEvent::Wheel) {
 | |
| 			handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
 | |
| 		}
 | |
| 		return base::EventFilterResult::Continue;
 | |
| 	});
 | |
| 
 | |
| 	if constexpr (Platform::IsWindows()) {
 | |
| 		_widget->setWindowFlags(Qt::FramelessWindowHint);
 | |
| 	} else if constexpr (Platform::IsMac()) {
 | |
| 		// Without Qt::Tool starting with Qt 5.15.1 this widget
 | |
| 		// when being opened from a fullscreen main window was
 | |
| 		// opening not as overlay over the main window, but as
 | |
| 		// a separate fullscreen window with a separate space.
 | |
| 		_widget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);
 | |
| 	}
 | |
| 	_widget->setAttribute(Qt::WA_NoSystemBackground, true);
 | |
| 	_widget->setAttribute(Qt::WA_TranslucentBackground, true);
 | |
| 	_widget->setMouseTracking(true);
 | |
| 
 | |
| 	hide();
 | |
| 	_widget->createWinId();
 | |
| 
 | |
| 	QObject::connect(
 | |
| 		window(),
 | |
| 		&QWindow::screenChanged,
 | |
| 		[=](QScreen *screen) { handleScreenChanged(screen); });
 | |
| 	subscribeToScreenGeometry();
 | |
| 	updateGeometry();
 | |
| 	updateControlsGeometry();
 | |
| 
 | |
| #ifdef Q_OS_MAC
 | |
| 	TouchBar::SetupMediaViewTouchBar(
 | |
| 		_widget->winId(),
 | |
| 		static_cast<PlaybackControls::Delegate*>(this),
 | |
| 		_touchbarTrackState.events(),
 | |
| 		_touchbarDisplay.events(),
 | |
| 		_touchbarFullscreenToggled.events());
 | |
| #endif // Q_OS_MAC
 | |
| 
 | |
| 	using namespace rpl::mappers;
 | |
| 	rpl::combine(
 | |
| 		Core::App().calls().currentCallValue(),
 | |
| 		Core::App().calls().currentGroupCallValue(),
 | |
| 		_1 || _2
 | |
| 	) | rpl::start_with_next([=](bool call) {
 | |
| 		if (!_streamed || videoIsGifOrUserpic()) {
 | |
| 			return;
 | |
| 		} else if (call) {
 | |
| 			playbackPauseOnCall();
 | |
| 		} else {
 | |
| 			playbackResumeOnCall();
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	_widget->setAttribute(Qt::WA_AcceptTouchEvents);
 | |
| 	_touchTimer.setCallback([=] { handleTouchTimer(); });
 | |
| 
 | |
| 	_controlsHideTimer.setCallback([=] { hideControls(); });
 | |
| 
 | |
| 	_docDownload->addClickHandler([=] { downloadMedia(); });
 | |
| 	_docSaveAs->addClickHandler([=] { saveAs(); });
 | |
| 	_docCancel->addClickHandler([=] { saveCancel(); });
 | |
| 
 | |
| 	_dropdown->setHiddenCallback([this] { dropdownHidden(); });
 | |
| 	_dropdownShowTimer.setCallback([=] { showDropdown(); });
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshLang() {
 | |
| 	InvokeQueued(_widget, [=] { updateThemePreviewGeometry(); });
 | |
| }
 | |
| 
 | |
| void OverlayWidget::moveToScreen(bool inMove) {
 | |
| 	const auto widgetScreen = [&](auto &&widget) -> QScreen* {
 | |
| 		if (!widget) {
 | |
| 			return nullptr;
 | |
| 		}
 | |
| 		if (!Platform::IsWayland()) {
 | |
| 			if (const auto screen = QGuiApplication::screenAt(
 | |
| 				widget->geometry().center())) {
 | |
| 				return screen;
 | |
| 			}
 | |
| 		}
 | |
| 		return widget->screen();
 | |
| 	};
 | |
| 	const auto applicationWindow = Core::App().activeWindow()
 | |
| 		? Core::App().activeWindow()->widget().get()
 | |
| 		: nullptr;
 | |
| 	const auto activeWindowScreen = widgetScreen(applicationWindow);
 | |
| 	const auto myScreen = widgetScreen(_widget);
 | |
| 	if (activeWindowScreen && myScreen != activeWindowScreen) {
 | |
| 		const auto screenList = QGuiApplication::screens();
 | |
| 		DEBUG_LOG(("Viewer Pos: Currently on screen %1, moving to screen %2")
 | |
| 			.arg(screenList.indexOf(myScreen))
 | |
| 			.arg(screenList.indexOf(activeWindowScreen)));
 | |
| 		window()->setScreen(activeWindowScreen);
 | |
| 		DEBUG_LOG(("Viewer Pos: New actual screen: %1")
 | |
| 			.arg(screenList.indexOf(_widget->screen())));
 | |
| 	}
 | |
| 	updateGeometry(inMove);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateGeometry(bool inMove) {
 | |
| 	if (Platform::IsWayland()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto available = _widget->screen()->geometry();
 | |
| 	const auto openglWidget = _opengl
 | |
| 		? static_cast<QOpenGLWidget*>(_widget.get())
 | |
| 		: nullptr;
 | |
| 	const auto possibleSizeHack = Platform::IsWindows() && openglWidget;
 | |
| 	const auto useSizeHack = possibleSizeHack
 | |
| 		&& (openglWidget->format().renderableType()
 | |
| 			!= QSurfaceFormat::OpenGLES);
 | |
| 	const auto use = useSizeHack
 | |
| 		? available.marginsAdded({ 0, 0, 0, 1 })
 | |
| 		: available;
 | |
| 	const auto mask = useSizeHack
 | |
| 		? QRegion(QRect(QPoint(), available.size()))
 | |
| 		: QRegion();
 | |
| 	if (inMove && use.contains(_widget->geometry())) {
 | |
| 		return;
 | |
| 	}
 | |
| 	if ((_widget->geometry() == use)
 | |
| 		&& (!possibleSizeHack || _widget->mask() == mask)) {
 | |
| 		return;
 | |
| 	}
 | |
| 	DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4")
 | |
| 		.arg(use.x())
 | |
| 		.arg(use.y())
 | |
| 		.arg(use.width())
 | |
| 		.arg(use.height()));
 | |
| 	_widget->setGeometry(use);
 | |
| 	if (possibleSizeHack) {
 | |
| 		_widget->setMask(mask);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateControlsGeometry() {
 | |
| 	auto navSkip = 2 * st::mediaviewControlMargin + st::mediaviewControlSize;
 | |
| 	_closeNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, st::mediaviewControlMargin, st::mediaviewControlSize, st::mediaviewControlSize);
 | |
| 	_closeNavIcon = style::centerrect(_closeNav, st::mediaviewClose);
 | |
| 	_leftNav = QRect(st::mediaviewControlMargin, navSkip, st::mediaviewControlSize, height() - 2 * navSkip);
 | |
| 	_leftNavIcon = style::centerrect(_leftNav, st::mediaviewLeft);
 | |
| 	_rightNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip);
 | |
| 	_rightNavIcon = style::centerrect(_rightNav, st::mediaviewRight);
 | |
| 
 | |
| 	_saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2);
 | |
| 	_photoRadialRect = QRect(QPoint((width() - st::radialSize.width()) / 2, (height() - st::radialSize.height()) / 2), st::radialSize);
 | |
| 
 | |
| 	updateControls();
 | |
| 	resizeContentByScreenSize();
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| QSize OverlayWidget::flipSizeByRotation(QSize size) const {
 | |
| 	return FlipSizeByRotation(size, _rotation);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::hasCopyMediaRestriction() const {
 | |
| 	return (_history && !_history->peer->allowsForwarding())
 | |
| 		|| (_message && _message->forbidsSaving());
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::showCopyMediaRestriction() {
 | |
| 	if (!hasCopyMediaRestriction()) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	Ui::ShowMultilineToast({
 | |
| 		.parentOverride = _widget,
 | |
| 		.text = { _history->peer->isBroadcast()
 | |
| 			? tr::lng_error_nocopy_channel(tr::now)
 | |
| 			: tr::lng_error_nocopy_group(tr::now) },
 | |
| 	});
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::videoShown() const {
 | |
| 	return _streamed && !_streamed->instance.info().video.cover.isNull();
 | |
| }
 | |
| 
 | |
| QSize OverlayWidget::videoSize() const {
 | |
| 	Expects(videoShown());
 | |
| 
 | |
| 	return flipSizeByRotation(_streamed->instance.info().video.size);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::videoIsGifOrUserpic() const {
 | |
| 	return _streamed
 | |
| 		&& (!_document
 | |
| 			|| (_document->isAnimation() && !_document->isVideoMessage()));
 | |
| }
 | |
| 
 | |
| QImage OverlayWidget::videoFrame() const {
 | |
| 	Expects(videoShown());
 | |
| 
 | |
| 	auto request = Streaming::FrameRequest();
 | |
| 	//request.radius = (_document && _document->isVideoMessage())
 | |
| 	//	? ImageRoundRadius::Ellipse
 | |
| 	//	: ImageRoundRadius::None;
 | |
| 	return _streamed->instance.player().ready()
 | |
| 		? _streamed->instance.frame(request)
 | |
| 		: _streamed->instance.info().video.cover;
 | |
| }
 | |
| 
 | |
| Streaming::FrameWithInfo OverlayWidget::videoFrameWithInfo() const {
 | |
| 	Expects(videoShown());
 | |
| 
 | |
| 	return _streamed->instance.player().ready()
 | |
| 		? _streamed->instance.frameWithInfo()
 | |
| 		: Streaming::FrameWithInfo{
 | |
| 			.image = _streamed->instance.info().video.cover,
 | |
| 			.format = Streaming::FrameFormat::ARGB32,
 | |
| 			.index = -2,
 | |
| 			.alpha = _streamed->instance.info().video.alpha,
 | |
| 		};
 | |
| }
 | |
| 
 | |
| QImage OverlayWidget::currentVideoFrameImage() const {
 | |
| 	return _streamed->instance.player().ready()
 | |
| 		? _streamed->instance.player().currentFrameImage()
 | |
| 		: _streamed->instance.info().video.cover;
 | |
| }
 | |
| 
 | |
| int OverlayWidget::streamedIndex() const {
 | |
| 	return _streamedCreated;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::documentContentShown() const {
 | |
| 	return _document && (!_staticContent.isNull() || videoShown());
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::documentBubbleShown() const {
 | |
| 	return (!_photo && !_document)
 | |
| 		|| (_document
 | |
| 			&& !_themePreviewShown
 | |
| 			&& !_streamed
 | |
| 			&& _staticContent.isNull());
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setStaticContent(QImage image) {
 | |
| 	constexpr auto kGood = QImage::Format_ARGB32_Premultiplied;
 | |
| 	if (!image.isNull()
 | |
| 		&& image.format() != kGood
 | |
| 		&& image.format() != QImage::Format_RGB32) {
 | |
| 		image = std::move(image).convertToFormat(kGood);
 | |
| 	}
 | |
| 	image.setDevicePixelRatio(cRetinaFactor());
 | |
| 	_staticContent = std::move(image);
 | |
| 	_staticContentTransparent = IsSemitransparent(_staticContent);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::contentShown() const {
 | |
| 	return _photo || documentContentShown();
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::opaqueContentShown() const {
 | |
| 	return contentShown()
 | |
| 		&& (!_staticContentTransparent
 | |
| 			|| !_document
 | |
| 			|| (!_document->isVideoMessage()
 | |
| 				&& !_document->sticker()
 | |
| 				&& (!_streamed || !_streamed->instance.info().video.alpha)));
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clearStreaming(bool savePosition) {
 | |
| 	if (_streamed && _document && savePosition) {
 | |
| 		Media::Player::SaveLastPlaybackPosition(
 | |
| 			_document,
 | |
| 			_streamed->instance.player().prepareLegacyState());
 | |
| 	}
 | |
| 	_fullScreenVideo = false;
 | |
| 	_streamed = nullptr;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::documentUpdated(not_null<DocumentData*> document) {
 | |
| 	if (_document != document) {
 | |
| 		return;
 | |
| 	} else if (documentBubbleShown()) {
 | |
| 		if ((_document->loading() && _docCancel->isHidden()) || (!_document->loading() && !_docCancel->isHidden())) {
 | |
| 			updateControls();
 | |
| 		} else if (_document->loading()) {
 | |
| 			updateDocSize();
 | |
| 			_widget->update(_docRect);
 | |
| 		}
 | |
| 	} else if (_streamed) {
 | |
| 		const auto ready = _documentMedia->loaded()
 | |
| 			? _document->size
 | |
| 			: _document->loading()
 | |
| 			? std::clamp(_document->loadOffset(), int64(), _document->size)
 | |
| 			: 0;
 | |
| 		_streamed->controls.setLoadingProgress(ready, _document->size);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::changingMsgId(FullMsgId newId, MsgId oldId) {
 | |
| 	if (_message && _message->fullId() == newId) {
 | |
| 		refreshMediaViewer();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateDocSize() {
 | |
| 	if (!_document || !documentBubbleShown()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto size = _document->size;
 | |
| 	_docSize = _document->loading()
 | |
| 		? Ui::FormatProgressText(_document->loadOffset(), size)
 | |
| 		: Ui::FormatSizeText(size);
 | |
| 	_docSizeWidth = st::mediaviewFont->width(_docSize);
 | |
| 	int32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;
 | |
| 	if (_docSizeWidth > maxw) {
 | |
| 		_docSize = st::mediaviewFont->elided(_docSize, maxw);
 | |
| 		_docSizeWidth = st::mediaviewFont->width(_docSize);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshNavVisibility() {
 | |
| 	if (_sharedMediaData) {
 | |
| 		_leftNavVisible = _index && (*_index > 0);
 | |
| 		_rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size());
 | |
| 	} else if (_userPhotosData) {
 | |
| 		_leftNavVisible = _index && (*_index > 0);
 | |
| 		_rightNavVisible = _index && (*_index + 1 < _userPhotosData->size());
 | |
| 	} else if (_collageData) {
 | |
| 		_leftNavVisible = _index && (*_index > 0);
 | |
| 		_rightNavVisible = _index && (*_index + 1 < _collageData->items.size());
 | |
| 	} else {
 | |
| 		_leftNavVisible = false;
 | |
| 		_rightNavVisible = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::contentCanBeSaved() const {
 | |
| 	if (hasCopyMediaRestriction()) {
 | |
| 		return false;
 | |
| 	} else if (_photo) {
 | |
| 		return _photo->hasVideo() || _photoMedia->loaded();
 | |
| 	} else if (_document) {
 | |
| 		return _document->filepath(true).isEmpty() && !_document->loading();
 | |
| 	} else {
 | |
| 		return false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::checkForSaveLoaded() {
 | |
| 	if (_savePhotoVideoWhenLoaded == SavePhotoVideo::None) {
 | |
| 		return;
 | |
| 	} else if (!_photo
 | |
| 		|| !_photo->hasVideo()
 | |
| 		|| _photoMedia->videoContent(Data::PhotoSize::Large).isEmpty()) {
 | |
| 		return;
 | |
| 	} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::QuickSave) {
 | |
| 		_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 | |
| 		downloadMedia();
 | |
| 	} else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::SaveAs) {
 | |
| 		_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 | |
| 		saveAs();
 | |
| 	} else {
 | |
| 		Unexpected("SavePhotoVideo in OverlayWidget::checkForSaveLoaded.");
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateControls() {
 | |
| 	if (_document && documentBubbleShown()) {
 | |
| 		_docRect = QRect(
 | |
| 			(width() - st::mediaviewFileSize.width()) / 2,
 | |
| 			(height() - st::mediaviewFileSize.height()) / 2,
 | |
| 			st::mediaviewFileSize.width(),
 | |
| 			st::mediaviewFileSize.height());
 | |
| 		_docIconRect = QRect(
 | |
| 			_docRect.x() + st::mediaviewFilePadding,
 | |
| 			_docRect.y() + st::mediaviewFilePadding,
 | |
| 			st::mediaviewFileIconSize,
 | |
| 			st::mediaviewFileIconSize);
 | |
| 		if (_document->loading()) {
 | |
| 			_docDownload->hide();
 | |
| 			_docSaveAs->hide();
 | |
| 			_docCancel->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
 | |
| 			_docCancel->show();
 | |
| 		} else {
 | |
| 			if (_documentMedia->loaded(true)) {
 | |
| 				_docDownload->hide();
 | |
| 				_docSaveAs->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
 | |
| 				_docSaveAs->show();
 | |
| 				_docCancel->hide();
 | |
| 			} else {
 | |
| 				_docDownload->moveToLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
 | |
| 				_docDownload->show();
 | |
| 				_docSaveAs->moveToLeft(_docRect.x() + 2.5 * st::mediaviewFilePadding + st::mediaviewFileIconSize + _docDownload->width(), _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileLinksTop);
 | |
| 				_docSaveAs->show();
 | |
| 				_docCancel->hide();
 | |
| 			}
 | |
| 		}
 | |
| 		updateDocSize();
 | |
| 	} else {
 | |
| 		_docIconRect = QRect(
 | |
| 			(width() - st::mediaviewFileIconSize) / 2,
 | |
| 			(height() - st::mediaviewFileIconSize) / 2,
 | |
| 			st::mediaviewFileIconSize,
 | |
| 			st::mediaviewFileIconSize);
 | |
| 		_docDownload->hide();
 | |
| 		_docSaveAs->hide();
 | |
| 		_docCancel->hide();
 | |
| 	}
 | |
| 	radialStart();
 | |
| 
 | |
| 	updateThemePreviewGeometry();
 | |
| 
 | |
| 	_saveVisible = contentCanBeSaved();
 | |
| 	_rotateVisible = !_themePreviewShown;
 | |
| 	const auto navRect = [&](int i) {
 | |
| 		return QRect(width() - st::mediaviewIconSize.width() * i,
 | |
| 			height() - st::mediaviewIconSize.height(),
 | |
| 			st::mediaviewIconSize.width(),
 | |
| 			st::mediaviewIconSize.height());
 | |
| 	};
 | |
| 	_saveNav = navRect(_rotateVisible ? 3 : 2);
 | |
| 	_saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave);
 | |
| 	_rotateNav = navRect(2);
 | |
| 	_rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate);
 | |
| 	_moreNav = navRect(1);
 | |
| 	_moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore);
 | |
| 
 | |
| 	const auto dNow = QDateTime::currentDateTime();
 | |
| 	const auto d = [&] {
 | |
| 		if (_message) {
 | |
| 			return ItemDateTime(_message);
 | |
| 		} else if (_photo) {
 | |
| 			return base::unixtime::parse(_photo->date);
 | |
| 		} else if (_document) {
 | |
| 			return base::unixtime::parse(_document->date);
 | |
| 		}
 | |
| 		return dNow;
 | |
| 	}();
 | |
| 	_dateText = Ui::FormatDateTime(d);
 | |
| 	if (!_fromName.isEmpty()) {
 | |
| 		_fromNameLabel.setText(st::mediaviewTextStyle, _fromName, Ui::NameTextOptions());
 | |
| 		_nameNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height);
 | |
| 		_dateNav = QRect(st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height);
 | |
| 	} else {
 | |
| 		_nameNav = QRect();
 | |
| 		_dateNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height);
 | |
| 	}
 | |
| 	updateHeader();
 | |
| 	refreshNavVisibility();
 | |
| 	resizeCenteredControls();
 | |
| 
 | |
| 	updateOver(_widget->mapFromGlobal(QCursor::pos()));
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::resizeCenteredControls() {
 | |
| 	const auto bottomSkip = std::max(
 | |
| 		_dateNav.left() + _dateNav.width(),
 | |
| 		_headerNav.left() + _headerNav.width())
 | |
| 		+ st::mediaviewCaptionMargin.width();
 | |
| 	_groupThumbsAvailableWidth = std::max(
 | |
| 		width() - 2 * bottomSkip,
 | |
| 		st::msgMinWidth
 | |
| 		+ st::mediaviewCaptionPadding.left()
 | |
| 		+ st::mediaviewCaptionPadding.right());
 | |
| 	_groupThumbsLeft = (width() - _groupThumbsAvailableWidth) / 2;
 | |
| 	refreshGroupThumbs();
 | |
| 	_groupThumbsTop = _groupThumbs ? (height() - _groupThumbs->height()) : 0;
 | |
| 
 | |
| 	refreshClipControllerGeometry();
 | |
| 	refreshCaptionGeometry();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshCaptionGeometry() {
 | |
| 	if (_caption.isEmpty()) {
 | |
| 		_captionRect = QRect();
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (_groupThumbs && _groupThumbs->hiding()) {
 | |
| 		_groupThumbs = nullptr;
 | |
| 		_groupThumbsRect = QRect();
 | |
| 	}
 | |
| 	const auto captionBottom = (_streamed && !videoIsGifOrUserpic())
 | |
| 		? (_streamed->controls.y() - st::mediaviewCaptionMargin.height())
 | |
| 		: _groupThumbs
 | |
| 		? _groupThumbsTop
 | |
| 		: height() - st::mediaviewCaptionMargin.height();
 | |
| 	const auto captionWidth = std::min(
 | |
| 		_groupThumbsAvailableWidth
 | |
| 		- st::mediaviewCaptionPadding.left()
 | |
| 		- st::mediaviewCaptionPadding.right(),
 | |
| 		_caption.maxWidth());
 | |
| 	const auto captionHeight = std::min(
 | |
| 		_caption.countHeight(captionWidth),
 | |
| 		height() / 4
 | |
| 		- st::mediaviewCaptionPadding.top()
 | |
| 		- st::mediaviewCaptionPadding.bottom()
 | |
| 		- 2 * st::mediaviewCaptionMargin.height());
 | |
| 	_captionRect = QRect(
 | |
| 		(width() - captionWidth) / 2,
 | |
| 		captionBottom
 | |
| 		- captionHeight
 | |
| 		- st::mediaviewCaptionPadding.bottom(),
 | |
| 		captionWidth,
 | |
| 		captionHeight);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
 | |
| 	if (_document && _document->loading()) {
 | |
| 		addAction(
 | |
| 			tr::lng_cancel(tr::now),
 | |
| 			[=] { saveCancel(); },
 | |
| 			&st::mediaMenuIconCancel);
 | |
| 	}
 | |
| 	if (_message && _message->isRegular()) {
 | |
| 		addAction(
 | |
| 			tr::lng_context_to_msg(tr::now),
 | |
| 			[=] { toMessage(); },
 | |
| 			&st::mediaMenuIconShowInChat);
 | |
| 	}
 | |
| 	if (_document && !_document->filepath(true).isEmpty()) {
 | |
| 		const auto text =  Platform::IsMac()
 | |
| 			? tr::lng_context_show_in_finder(tr::now)
 | |
| 			: tr::lng_context_show_in_folder(tr::now);
 | |
| 		addAction(
 | |
| 			text,
 | |
| 			[=] { showInFolder(); },
 | |
| 			&st::mediaMenuIconShowInFolder);
 | |
| 	}
 | |
| 	if (!hasCopyMediaRestriction()) {
 | |
| 		if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) {
 | |
| 			addAction(
 | |
| 				tr::lng_mediaview_copy(tr::now),
 | |
| 				[=] { copyMedia(); },
 | |
| 				&st::mediaMenuIconCopy);
 | |
| 		}
 | |
| 	}
 | |
| 	if ((_photo && _photo->hasAttachedStickers())
 | |
| 		|| (_document && _document->hasAttachedStickers())) {
 | |
| 		addAction(
 | |
| 			tr::lng_context_attached_stickers(tr::now),
 | |
| 			[=] { showAttachedStickers(); },
 | |
| 			&st::mediaMenuIconStickers);
 | |
| 	}
 | |
| 	if (_message && _message->allowsForward()) {
 | |
| 		addAction(
 | |
| 			tr::lng_mediaview_forward(tr::now),
 | |
| 			[=] { forwardMedia(); },
 | |
| 			&st::mediaMenuIconForward);
 | |
| 	}
 | |
| 	const auto canDelete = [&] {
 | |
| 		if (_message && _message->canDelete()) {
 | |
| 			return true;
 | |
| 		} else if (!_message
 | |
| 			&& _photo
 | |
| 			&& _user
 | |
| 			&& _user == _user->session().user()) {
 | |
| 			return _userPhotosData && _fullIndex && _fullCount;
 | |
| 		} else if (_photo && _photo->peer && _photo->peer->userpicPhotoId() == _photo->id) {
 | |
| 			if (auto chat = _photo->peer->asChat()) {
 | |
| 				return chat->canEditInformation();
 | |
| 			} else if (auto channel = _photo->peer->asChannel()) {
 | |
| 				return channel->canEditInformation();
 | |
| 			}
 | |
| 		}
 | |
| 		return false;
 | |
| 	}();
 | |
| 	if (canDelete) {
 | |
| 		addAction(
 | |
| 			tr::lng_mediaview_delete(tr::now),
 | |
| 			[=] { deleteMedia(); },
 | |
| 			&st::mediaMenuIconDelete);
 | |
| 	}
 | |
| 	if (!hasCopyMediaRestriction()) {
 | |
| 		addAction(
 | |
| 			tr::lng_mediaview_save_as(tr::now),
 | |
| 			[=] { saveAs(); },
 | |
| 			&st::mediaMenuIconDownload);
 | |
| 	}
 | |
| 
 | |
| 	if (const auto overviewType = computeOverviewType()) {
 | |
| 		const auto text = _document
 | |
| 			? tr::lng_mediaview_files_all(tr::now)
 | |
| 			: tr::lng_mediaview_photos_all(tr::now);
 | |
| 		addAction(
 | |
| 			text,
 | |
| 			[=] { showMediaOverview(); },
 | |
| 			&st::mediaMenuIconShowAll);
 | |
| 	}
 | |
| 	[&] { // Set userpic.
 | |
| 		if (!_peer || !_photo || (_peer->userpicPhotoId() == _photo->id)) {
 | |
| 			return;
 | |
| 		}
 | |
| 		using Type = SharedMediaType;
 | |
| 		if (sharedMediaType().value_or(Type::File) == Type::ChatPhoto) {
 | |
| 			if (const auto chat = _peer->asChat()) {
 | |
| 				if (!chat->canEditInformation()) {
 | |
| 					return;
 | |
| 				}
 | |
| 			} else if (const auto channel = _peer->asChannel()) {
 | |
| 				if (!channel->canEditInformation()) {
 | |
| 					return;
 | |
| 				}
 | |
| 			} else {
 | |
| 				return;
 | |
| 			}
 | |
| 		} else if (userPhotosKey()) {
 | |
| 			if (_user != _user->session().user()) {
 | |
| 				return;
 | |
| 			}
 | |
| 		} else {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto photo = _photo;
 | |
| 		const auto peer = _peer;
 | |
| 		addAction(tr::lng_mediaview_set_userpic(tr::now), [=] {
 | |
| 			auto lifetime = std::make_shared<rpl::lifetime>();
 | |
| 			peer->session().changes().peerFlagsValue(
 | |
| 				peer,
 | |
| 				Data::PeerUpdate::Flag::Photo
 | |
| 			) | rpl::start_with_next([=]() mutable {
 | |
| 				if (lifetime) {
 | |
| 					base::take(lifetime)->destroy();
 | |
| 				}
 | |
| 				close();
 | |
| 			}, *lifetime);
 | |
| 
 | |
| 			peer->session().api().peerPhoto().set(peer, photo);
 | |
| 		}, &st::mediaMenuIconProfile);
 | |
| 	}();
 | |
| 	[&] { // Report userpic.
 | |
| 		if (!_peer || !_photo ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		using Type = SharedMediaType;
 | |
| 		if (userPhotosKey()) {
 | |
| 			if (_peer->isSelf() || _peer->isNotificationsUser()) {
 | |
| 				return;
 | |
| 			}
 | |
| 		} else if ((sharedMediaType().value_or(Type::File) == Type::ChatPhoto)
 | |
| 			|| (_peer->userpicPhotoId() == _photo->id)) {
 | |
| 			if (const auto chat = _peer->asChat()) {
 | |
| 				if (chat->canEditInformation()) {
 | |
| 					return;
 | |
| 				}
 | |
| 			} else if (const auto channel = _peer->asChannel()) {
 | |
| 				if (channel->canEditInformation()) {
 | |
| 					return;
 | |
| 				}
 | |
| 			} else {
 | |
| 				return;
 | |
| 			}
 | |
| 		} else {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto photo = _photo;
 | |
| 		const auto peer = _peer;
 | |
| 		addAction(tr::lng_mediaview_report_profile_photo(tr::now), [=] {
 | |
| 			if (const auto window = findWindow()) {
 | |
| 				close();
 | |
| 				window->show(
 | |
| 					ReportProfilePhotoBox(peer, photo),
 | |
| 					Ui::LayerOption::CloseOther);
 | |
| 			}
 | |
| 		}, &st::mediaMenuIconReport);
 | |
| 	}();
 | |
| }
 | |
| 
 | |
| auto OverlayWidget::computeOverviewType() const
 | |
| -> std::optional<SharedMediaType> {
 | |
| 	if (const auto mediaType = sharedMediaType()) {
 | |
| 		if (const auto overviewType = SharedMediaOverviewType(*mediaType)) {
 | |
| 			return overviewType;
 | |
| 		} else if (mediaType == SharedMediaType::PhotoVideo) {
 | |
| 			if (_photo) {
 | |
| 				return SharedMediaOverviewType(SharedMediaType::Photo);
 | |
| 			} else if (_document) {
 | |
| 				return SharedMediaOverviewType(SharedMediaType::Video);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return std::nullopt;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::stateAnimationCallback(crl::time now) {
 | |
| 	if (anim::Disabled()) {
 | |
| 		now += st::mediaviewShowDuration + st::mediaviewHideDuration;
 | |
| 	}
 | |
| 	for (auto i = begin(_animations); i != end(_animations);) {
 | |
| 		const auto [state, started] = *i;
 | |
| 		updateOverRect(state);
 | |
| 		const auto dt = float64(now - started) / st::mediaviewFadeDuration;
 | |
| 		if (dt >= 1) {
 | |
| 			_animationOpacities.erase(state);
 | |
| 			i = _animations.erase(i);
 | |
| 		} else {
 | |
| 			_animationOpacities[state].update(dt, anim::linear);
 | |
| 			++i;
 | |
| 		}
 | |
| 	}
 | |
| 	return !_animations.empty() || updateControlsAnimation(now);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::updateControlsAnimation(crl::time now) {
 | |
| 	if (_controlsState != ControlsShowing
 | |
| 		&& _controlsState != ControlsHiding) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	const auto duration = (_controlsState == ControlsShowing)
 | |
| 		? st::mediaviewShowDuration
 | |
| 		: st::mediaviewHideDuration;
 | |
| 	const auto dt = float64(now - _controlsAnimStarted)
 | |
| 		/ duration;
 | |
| 	if (dt >= 1) {
 | |
| 		_controlsOpacity.finish();
 | |
| 		_controlsState = (_controlsState == ControlsShowing)
 | |
| 			? ControlsShown
 | |
| 			: ControlsHidden;
 | |
| 		updateCursor();
 | |
| 	} else {
 | |
| 		_controlsOpacity.update(dt, anim::linear);
 | |
| 	}
 | |
| 	const auto toUpdate = QRegion()
 | |
| 		+ (_over == OverLeftNav ? _leftNav : _leftNavIcon)
 | |
| 		+ (_over == OverRightNav ? _rightNav : _rightNavIcon)
 | |
| 		+ (_over == OverClose ? _closeNav : _closeNavIcon)
 | |
| 		+ _saveNavIcon
 | |
| 		+ _rotateNavIcon
 | |
| 		+ _moreNavIcon
 | |
| 		+ _headerNav
 | |
| 		+ _nameNav
 | |
| 		+ _dateNav
 | |
| 		+ _captionRect.marginsAdded(st::mediaviewCaptionPadding)
 | |
| 		+ _groupThumbsRect;
 | |
| 	update(toUpdate);
 | |
| 	return (dt < 1);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::waitingAnimationCallback() {
 | |
| 	if (!anim::Disabled()) {
 | |
| 		update(radialRect());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateCursor() {
 | |
| 	setCursor(_controlsState == ControlsHidden
 | |
| 		? Qt::BlankCursor
 | |
| 		: (_over == OverNone ? style::cur_default : style::cur_pointer));
 | |
| }
 | |
| 
 | |
| int OverlayWidget::finalContentRotation() const {
 | |
| 	return _streamed
 | |
| 		? ((_rotation + (_streamed
 | |
| 			? _streamed->instance.info().video.rotation
 | |
| 			: 0)) % 360)
 | |
| 		: _rotation;
 | |
| }
 | |
| 
 | |
| QRect OverlayWidget::finalContentRect() const {
 | |
| 	return { _x, _y, _w, _h };
 | |
| }
 | |
| 
 | |
| OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const {
 | |
| 	const auto toRotation = qreal(finalContentRotation());
 | |
| 	const auto toRectRotated = QRectF(finalContentRect());
 | |
| 	const auto toRectCenter = toRectRotated.center();
 | |
| 	const auto toRect = ((int(toRotation) % 180) == 90)
 | |
| 		? QRectF(
 | |
| 			toRectCenter.x() - toRectRotated.height() / 2.,
 | |
| 			toRectCenter.y() - toRectRotated.width() / 2.,
 | |
| 			toRectRotated.height(),
 | |
| 			toRectRotated.width())
 | |
| 		: toRectRotated;
 | |
| 	if (!_geometryAnimation.animating()) {
 | |
| 		return { toRect, toRotation };
 | |
| 	}
 | |
| 	const auto fromRect = _oldGeometry.rect;
 | |
| 	const auto fromRotation = _oldGeometry.rotation;
 | |
| 	const auto progress = _geometryAnimation.value(1.);
 | |
| 	const auto rotationDelta = (toRotation - fromRotation);
 | |
| 	const auto useRotationDelta = (rotationDelta > 180.)
 | |
| 		? (rotationDelta - 360.)
 | |
| 		: (rotationDelta <= -180.)
 | |
| 		? (rotationDelta + 360.)
 | |
| 		: rotationDelta;
 | |
| 	const auto rotation = fromRotation + useRotationDelta * progress;
 | |
| 	const auto useRotation = (rotation > 360.)
 | |
| 		? (rotation - 360.)
 | |
| 		: (rotation < 0.)
 | |
| 		? (rotation + 360.)
 | |
| 		: rotation;
 | |
| 	const auto useRect = QRectF(
 | |
| 		fromRect.x() + (toRect.x() - fromRect.x()) * progress,
 | |
| 		fromRect.y() + (toRect.y() - fromRect.y()) * progress,
 | |
| 		fromRect.width() + (toRect.width() - fromRect.width()) * progress,
 | |
| 		fromRect.height() + (toRect.height() - fromRect.height()) * progress
 | |
| 	);
 | |
| 	return { useRect, useRotation };
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateContentRect() {
 | |
| 	if (_opengl) {
 | |
| 		update();
 | |
| 	} else {
 | |
| 		update(finalContentRect());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::contentSizeChanged() {
 | |
| 	_width = _w;
 | |
| 	_height = _h;
 | |
| 	resizeContentByScreenSize();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::resizeContentByScreenSize() {
 | |
| 	const auto bottom = (!_streamed || videoIsGifOrUserpic())
 | |
| 		? height()
 | |
| 		: (_streamed->controls.y()
 | |
| 			- st::mediaviewCaptionPadding.bottom()
 | |
| 			- st::mediaviewCaptionMargin.height());
 | |
| 	const auto skipHeight = (height() - bottom);
 | |
| 	const auto availableWidth = width();
 | |
| 	const auto availableHeight = height() - 2 * skipHeight;
 | |
| 	const auto countZoomFor = [&](int outerw, int outerh) {
 | |
| 		auto result = float64(outerw) / _width;
 | |
| 		if (_height * result > outerh) {
 | |
| 			result = float64(outerh) / _height;
 | |
| 		}
 | |
| 		if (result >= 1.) {
 | |
| 			result -= 1.;
 | |
| 		} else {
 | |
| 			result = 1. - (1. / result);
 | |
| 		}
 | |
| 		return result;
 | |
| 	};
 | |
| 	if (_width > 0 && _height > 0) {
 | |
| 		_zoomToDefault = countZoomFor(availableWidth, availableHeight);
 | |
| 		_zoomToScreen = countZoomFor(width(), height());
 | |
| 	} else {
 | |
| 		_zoomToDefault = _zoomToScreen = 0;
 | |
| 	}
 | |
| 	const auto usew = _fullScreenVideo ? width() : availableWidth;
 | |
| 	const auto useh = _fullScreenVideo ? height() : availableHeight;
 | |
| 	if ((_width > usew) || (_height > useh) || _fullScreenVideo) {
 | |
| 		const auto use = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
 | |
| 		_zoom = kZoomToScreenLevel;
 | |
| 		if (use >= 0) {
 | |
| 			_w = qRound(_width * (use + 1));
 | |
| 			_h = qRound(_height * (use + 1));
 | |
| 		} else {
 | |
| 			_w = qRound(_width / (-use + 1));
 | |
| 			_h = qRound(_height / (-use + 1));
 | |
| 		}
 | |
| 	} else {
 | |
| 		_zoom = 0;
 | |
| 		_w = _width;
 | |
| 		_h = _height;
 | |
| 	}
 | |
| 	_x = (width() - _w) / 2;
 | |
| 	_y = (height() - _h) / 2;
 | |
| 	_geometryAnimation.stop();
 | |
| }
 | |
| 
 | |
| float64 OverlayWidget::radialProgress() const {
 | |
| 	if (_document) {
 | |
| 		return _documentMedia->progress();
 | |
| 	} else if (_photo) {
 | |
| 		return _photoMedia->progress();
 | |
| 	}
 | |
| 	return 1.;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::radialLoading() const {
 | |
| 	if (_streamed) {
 | |
| 		return false;
 | |
| 	} else if (_document) {
 | |
| 		return _document->loading();
 | |
| 	} else if (_photo) {
 | |
| 		return _photo->displayLoading();
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| QRect OverlayWidget::radialRect() const {
 | |
| 	if (_photo) {
 | |
| 		return _photoRadialRect;
 | |
| 	} else if (_document) {
 | |
| 		return QRect(
 | |
| 			QPoint(
 | |
| 				_docIconRect.x() + ((_docIconRect.width() - st::radialSize.width()) / 2),
 | |
| 				_docIconRect.y() + ((_docIconRect.height() - st::radialSize.height()) / 2)),
 | |
| 			st::radialSize);
 | |
| 	}
 | |
| 	return QRect();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::radialStart() {
 | |
| 	if (radialLoading() && !_radial.animating()) {
 | |
| 		_radial.start(radialProgress());
 | |
| 		if (auto shift = radialTimeShift()) {
 | |
| 			_radial.update(radialProgress(), !radialLoading(), crl::now() + shift);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| crl::time OverlayWidget::radialTimeShift() const {
 | |
| 	return _photo ? st::radialDuration : 0;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::radialAnimationCallback(crl::time now) {
 | |
| 	if ((!_document && !_photo) || _streamed) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	const auto wasAnimating = _radial.animating();
 | |
| 	const auto updated = _radial.update(
 | |
| 		radialProgress(),
 | |
| 		!radialLoading(),
 | |
| 		now + radialTimeShift());
 | |
| 	if ((wasAnimating || _radial.animating())
 | |
| 		&& (!anim::Disabled() || updated)) {
 | |
| 		update(radialRect());
 | |
| 	}
 | |
| 	const auto ready = _document && _documentMedia->loaded();
 | |
| 	const auto streamVideo = ready && _documentMedia->canBePlayed(_message);
 | |
| 	const auto tryOpenImage = ready
 | |
| 		&& (_document->size < Images::kReadBytesLimit);
 | |
| 	if (ready && ((tryOpenImage && !_radial.animating()) || streamVideo)) {
 | |
| 		_streamingStartPaused = false;
 | |
| 		if (streamVideo) {
 | |
| 			redisplayContent();
 | |
| 		} else {
 | |
| 			auto &location = _document->location(true);
 | |
| 			if (location.accessEnable()) {
 | |
| 				if (_document->isTheme()
 | |
| 					|| QImageReader(location.name()).canRead()) {
 | |
| 					redisplayContent();
 | |
| 				}
 | |
| 				location.accessDisable();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::zoomIn() {
 | |
| 	auto newZoom = _zoom;
 | |
| 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
 | |
| 	if (newZoom == kZoomToScreenLevel) {
 | |
| 		if (qCeil(full) <= kMaxZoomLevel) {
 | |
| 			newZoom = qCeil(full);
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (newZoom < full && (newZoom + 1 > full || (full > kMaxZoomLevel && newZoom == kMaxZoomLevel))) {
 | |
| 			newZoom = kZoomToScreenLevel;
 | |
| 		} else if (newZoom < kMaxZoomLevel) {
 | |
| 			++newZoom;
 | |
| 		}
 | |
| 	}
 | |
| 	zoomUpdate(newZoom);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::zoomOut() {
 | |
| 	auto newZoom = _zoom;
 | |
| 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
 | |
| 	if (newZoom == kZoomToScreenLevel) {
 | |
| 		if (qFloor(full) >= -kMaxZoomLevel) {
 | |
| 			newZoom = qFloor(full);
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (newZoom > full && (newZoom - 1 < full || (full < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) {
 | |
| 			newZoom = kZoomToScreenLevel;
 | |
| 		} else if (newZoom > -kMaxZoomLevel) {
 | |
| 			--newZoom;
 | |
| 		}
 | |
| 	}
 | |
| 	zoomUpdate(newZoom);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::zoomReset() {
 | |
| 	auto newZoom = _zoom;
 | |
| 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
 | |
| 	if (_zoom == 0) {
 | |
| 		if (qFloor(full) == qCeil(full) && qRound(full) >= -kMaxZoomLevel && qRound(full) <= kMaxZoomLevel) {
 | |
| 			newZoom = qRound(full);
 | |
| 		} else {
 | |
| 			newZoom = kZoomToScreenLevel;
 | |
| 		}
 | |
| 	} else {
 | |
| 		newZoom = 0;
 | |
| 	}
 | |
| 	_x = -_width / 2;
 | |
| 	_y = -_height / 2;
 | |
| 	float64 z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
 | |
| 	if (z >= 0) {
 | |
| 		_x = qRound(_x * (z + 1));
 | |
| 		_y = qRound(_y * (z + 1));
 | |
| 	} else {
 | |
| 		_x = qRound(_x / (-z + 1));
 | |
| 		_y = qRound(_y / (-z + 1));
 | |
| 	}
 | |
| 	_x += width() / 2;
 | |
| 	_y += height() / 2;
 | |
| 	update();
 | |
| 	zoomUpdate(newZoom);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::zoomUpdate(int32 &newZoom) {
 | |
| 	if (newZoom != kZoomToScreenLevel) {
 | |
| 		while ((newZoom < 0 && (-newZoom + 1) > _w) || (-newZoom + 1) > _h) {
 | |
| 			++newZoom;
 | |
| 		}
 | |
| 	}
 | |
| 	setZoomLevel(newZoom);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clearSession() {
 | |
| 	if (!isHidden()) {
 | |
| 		hide();
 | |
| 	}
 | |
| 	_sessionLifetime.destroy();
 | |
| 	if (!_animations.empty()) {
 | |
| 		_animations.clear();
 | |
| 		_stateAnimation.stop();
 | |
| 	}
 | |
| 	if (!_animationOpacities.empty()) {
 | |
| 		_animationOpacities.clear();
 | |
| 	}
 | |
| 	clearStreaming();
 | |
| 	setContext(v::null);
 | |
| 	_from = nullptr;
 | |
| 	_fromName = QString();
 | |
| 	assignMediaPointer(nullptr);
 | |
| 	_fullScreenVideo = false;
 | |
| 	_caption.clear();
 | |
| 	_sharedMedia = nullptr;
 | |
| 	_userPhotos = nullptr;
 | |
| 	_collage = nullptr;
 | |
| 	_session = nullptr;
 | |
| }
 | |
| 
 | |
| OverlayWidget::~OverlayWidget() {
 | |
| 	clearSession();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::assignMediaPointer(DocumentData *document) {
 | |
| 	_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 | |
| 	_photo = nullptr;
 | |
| 	_photoMedia = nullptr;
 | |
| 	if (_document != document) {
 | |
| 		if ((_document = document)) {
 | |
| 			_documentMedia = _document->createMediaView();
 | |
| 			_documentMedia->goodThumbnailWanted();
 | |
| 			_documentMedia->thumbnailWanted(fileOrigin());
 | |
| 		} else {
 | |
| 			_documentMedia = nullptr;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::assignMediaPointer(not_null<PhotoData*> photo) {
 | |
| 	_savePhotoVideoWhenLoaded = SavePhotoVideo::None;
 | |
| 	_document = nullptr;
 | |
| 	_documentMedia = nullptr;
 | |
| 	if (_photo != photo) {
 | |
| 		_photo = photo;
 | |
| 		_photoMedia = _photo->createMediaView();
 | |
| 		_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());
 | |
| 		if (!_photo->hasVideo() || _photo->videoPlaybackFailed()) {
 | |
| 			_photo->load(fileOrigin(), LoadFromCloudOrLocal, true);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
 | |
| 	setCursor((active || ClickHandler::getPressed()) ? style::cur_pointer : style::cur_default);
 | |
| 	update(QRegion(_saveMsg) + _captionRect);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
 | |
| 	setCursor((pressed || ClickHandler::getActive()) ? style::cur_pointer : style::cur_default);
 | |
| 	update(QRegion(_saveMsg) + _captionRect);
 | |
| }
 | |
| 
 | |
| rpl::lifetime &OverlayWidget::lifetime() {
 | |
| 	return _surface->lifetime();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::showSaveMsgFile() {
 | |
| 	File::ShowInFolder(_saveMsgFilename);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::close() {
 | |
| 	Core::App().hideMediaView();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::activateControls() {
 | |
| 	if (!_menu && !_mousePressed) {
 | |
| 		_controlsHideTimer.callOnce(st::mediaviewWaitHide);
 | |
| 	}
 | |
| 	if (_fullScreenVideo) {
 | |
| 		if (_streamed) {
 | |
| 			_streamed->controls.showAnimated();
 | |
| 		}
 | |
| 	}
 | |
| 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) {
 | |
| 		_controlsState = ControlsShowing;
 | |
| 		_controlsAnimStarted = crl::now();
 | |
| 		_controlsOpacity.start(1);
 | |
| 		if (!_stateAnimation.animating()) {
 | |
| 			_stateAnimation.start();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::hideControls(bool force) {
 | |
| 	if (!force) {
 | |
| 		if (!_dropdown->isHidden()
 | |
| 			|| (_streamed && _streamed->controls.hasMenu())
 | |
| 			|| _menu
 | |
| 			|| _mousePressed
 | |
| 			|| (_fullScreenVideo
 | |
| 				&& !videoIsGifOrUserpic()
 | |
| 				&& _streamed->controls.geometry().contains(_lastMouseMovePos))) {
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	if (_fullScreenVideo) {
 | |
| 		_streamed->controls.hideAnimated();
 | |
| 	}
 | |
| 	if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return;
 | |
| 
 | |
| 	_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());
 | |
| 	_controlsState = ControlsHiding;
 | |
| 	_controlsAnimStarted = crl::now();
 | |
| 	_controlsOpacity.start(0);
 | |
| 	if (!_stateAnimation.animating()) {
 | |
| 		_stateAnimation.start();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::dropdownHidden() {
 | |
| 	setFocus();
 | |
| 	_ignoringDropdown = true;
 | |
| 	_lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos());
 | |
| 	updateOver(_lastMouseMovePos);
 | |
| 	_ignoringDropdown = false;
 | |
| 	if (!_controlsHideTimer.isActive()) {
 | |
| 		hideControls(true);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleScreenChanged(QScreen *screen) {
 | |
| 	subscribeToScreenGeometry();
 | |
| 	if (isHidden()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto screenList = QGuiApplication::screens();
 | |
| 	DEBUG_LOG(("Viewer Pos: Screen changed to: %1")
 | |
| 		.arg(screenList.indexOf(screen)));
 | |
| 
 | |
| 	moveToScreen();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::subscribeToScreenGeometry() {
 | |
| 	_screenGeometryLifetime.destroy();
 | |
| 	const auto screen = _widget->screen();
 | |
| 	if (!screen) {
 | |
| 		return;
 | |
| 	}
 | |
| 	base::qt_signal_producer(
 | |
| 		screen,
 | |
| 		&QScreen::geometryChanged
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		updateGeometry();
 | |
| 	}, _screenGeometryLifetime);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::toMessage() {
 | |
| 	if (const auto item = _message) {
 | |
| 		close();
 | |
| 		if (const auto window = findWindow()) {
 | |
| 			window->showMessage(item);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::notifyFileDialogShown(bool shown) {
 | |
| 	if (shown && isHidden()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	if (shown) {
 | |
| 		Ui::Platform::BringToBack(_widget);
 | |
| 	} else {
 | |
| 		Ui::Platform::ShowOverAll(_widget);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::saveAs() {
 | |
| 	if (showCopyMediaRestriction()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	QString file;
 | |
| 	if (_document) {
 | |
| 		const auto &location = _document->location(true);
 | |
| 		const auto bytes = _documentMedia->bytes();
 | |
| 		if (!bytes.isEmpty() || location.accessEnable()) {
 | |
| 			QFileInfo alreadyInfo(location.name());
 | |
| 			QDir alreadyDir(alreadyInfo.dir());
 | |
| 			QString name = alreadyInfo.fileName(), filter;
 | |
| 			const auto mimeType = Core::MimeTypeForName(_document->mimeString());
 | |
| 			QStringList p = mimeType.globPatterns();
 | |
| 			QString pattern = p.isEmpty() ? QString() : p.front();
 | |
| 			if (name.isEmpty()) {
 | |
| 				name = pattern.isEmpty() ? u".unknown"_q : pattern.replace('*', QString());
 | |
| 			}
 | |
| 
 | |
| 			if (pattern.isEmpty()) {
 | |
| 				filter = QString();
 | |
| 			} else {
 | |
| 				filter = mimeType.filterString() + u";;"_q + FileDialog::AllFilesFilter();
 | |
| 			}
 | |
| 
 | |
| 			file = FileNameForSave(
 | |
| 				_session,
 | |
| 				tr::lng_save_file(tr::now),
 | |
| 				filter,
 | |
| 				u"doc"_q,
 | |
| 				name,
 | |
| 				true,
 | |
| 				alreadyDir);
 | |
| 			if (!file.isEmpty() && file != location.name()) {
 | |
| 				if (bytes.isEmpty()) {
 | |
| 					QFile(file).remove();
 | |
| 					QFile(location.name()).copy(file);
 | |
| 				} else {
 | |
| 					QFile f(file);
 | |
| 					f.open(QIODevice::WriteOnly);
 | |
| 					f.write(bytes);
 | |
| 				}
 | |
| 				if (_message) {
 | |
| 					auto &manager = Core::App().downloadManager();
 | |
| 					manager.addLoaded({
 | |
| 						.item = _message,
 | |
| 						.document = _document,
 | |
| 					}, file, manager.computeNextStartDate());
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if (bytes.isEmpty()) {
 | |
| 				location.accessDisable();
 | |
| 			}
 | |
| 		} else {
 | |
| 			DocumentSaveClickHandler::SaveAndTrack(
 | |
| 				_message ? _message->fullId() : FullMsgId(),
 | |
| 				_document,
 | |
| 				DocumentSaveClickHandler::Mode::ToNewFile);
 | |
| 			updateControls();
 | |
| 			updateOver(_lastMouseMovePos);
 | |
| 		}
 | |
| 	} else if (_photo && _photo->hasVideo()) {
 | |
| 		constexpr auto large = Data::PhotoSize::Large;
 | |
| 		if (const auto bytes = _photoMedia->videoContent(large); !bytes.isEmpty()) {
 | |
| 			const auto photo = _photo;
 | |
| 			auto filter = u"Video Files (*.mp4);;"_q + FileDialog::AllFilesFilter();
 | |
| 			FileDialog::GetWritePath(
 | |
| 				_widget.get(),
 | |
| 				tr::lng_save_video(tr::now),
 | |
| 				filter,
 | |
| 				filedialogDefaultName(
 | |
| 					u"photo"_q,
 | |
| 					u".mp4"_q,
 | |
| 					QString(),
 | |
| 					false,
 | |
| 					_photo->date),
 | |
| 				crl::guard(_widget, [=](const QString &result) {
 | |
| 					QFile f(result);
 | |
| 					if (!result.isEmpty()
 | |
| 						&& _photo == photo
 | |
| 						&& f.open(QIODevice::WriteOnly)) {
 | |
| 						f.write(bytes);
 | |
| 					}
 | |
| 				}));
 | |
| 		} else {
 | |
| 			_photo->loadVideo(large, fileOrigin());
 | |
| 			_savePhotoVideoWhenLoaded = SavePhotoVideo::SaveAs;
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (!_photo || !_photoMedia->loaded()) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const auto media = _photoMedia;
 | |
| 		const auto photo = _photo;
 | |
| 		const auto filter = u"JPEG Image (*.jpg);;"_q
 | |
| 			+ FileDialog::AllFilesFilter();
 | |
| 		FileDialog::GetWritePath(
 | |
| 			_widget.get(),
 | |
| 			tr::lng_save_photo(tr::now),
 | |
| 			filter,
 | |
| 			filedialogDefaultName(
 | |
| 				u"photo"_q,
 | |
| 				u".jpg"_q,
 | |
| 				QString(),
 | |
| 				false,
 | |
| 				_photo->date),
 | |
| 			crl::guard(_widget, [=](const QString &result) {
 | |
| 				if (!result.isEmpty() && _photo == photo) {
 | |
| 					media->saveToFile(result);
 | |
| 				}
 | |
| 			}));
 | |
| 	}
 | |
| 	activate();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleDocumentClick() {
 | |
| 	if (_document->loading()) {
 | |
| 		saveCancel();
 | |
| 	} else {
 | |
| 		Data::ResolveDocument(
 | |
| 			findWindow(),
 | |
| 			_document,
 | |
| 			_message,
 | |
| 			_topicRootId);
 | |
| 		if (_document->loading() && !_radial.animating()) {
 | |
| 			_radial.start(_documentMedia->progress());
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::downloadMedia() {
 | |
| 	if (!_photo && !_document) {
 | |
| 		return;
 | |
| 	}
 | |
| 	if (Core::App().settings().askDownloadPath()) {
 | |
| 		return saveAs();
 | |
| 	}
 | |
| 
 | |
| 	QString path;
 | |
| 	const auto session = _photo ? &_photo->session() : &_document->session();
 | |
| 	if (Core::App().settings().downloadPath().isEmpty()) {
 | |
| 		path = File::DefaultDownloadPath(session);
 | |
| 	} else if (Core::App().settings().downloadPath() == FileDialog::Tmp()) {
 | |
| 		path = session->local().tempDirectory();
 | |
| 	} else {
 | |
| 		path = Core::App().settings().downloadPath();
 | |
| 	}
 | |
| 	if (path.isEmpty()) return;
 | |
| 	QString toName;
 | |
| 	if (_document) {
 | |
| 		const auto &location = _document->location(true);
 | |
| 		if (location.accessEnable()) {
 | |
| 			if (!QDir().exists(path)) QDir().mkpath(path);
 | |
| 			toName = filedialogNextFilename(
 | |
| 				_document->filename(),
 | |
| 				location.name(),
 | |
| 				path);
 | |
| 			if (!toName.isEmpty() && toName != location.name()) {
 | |
| 				QFile(toName).remove();
 | |
| 				if (!QFile(location.name()).copy(toName)) {
 | |
| 					toName = QString();
 | |
| 				} else if (_message) {
 | |
| 					auto &manager = Core::App().downloadManager();
 | |
| 					manager.addLoaded({
 | |
| 						.item = _message,
 | |
| 						.document = _document,
 | |
| 					}, toName, manager.computeNextStartDate());
 | |
| 				}
 | |
| 			}
 | |
| 			location.accessDisable();
 | |
| 		} else {
 | |
| 			if (_document->filepath(true).isEmpty()
 | |
| 				&& !_document->loading()) {
 | |
| 				DocumentSaveClickHandler::SaveAndTrack(
 | |
| 					_message ? _message->fullId() : FullMsgId(),
 | |
| 					_document,
 | |
| 					DocumentSaveClickHandler::Mode::ToFile);
 | |
| 				updateControls();
 | |
| 			} else {
 | |
| 				_saveVisible = contentCanBeSaved();
 | |
| 				update(_saveNav);
 | |
| 			}
 | |
| 			updateOver(_lastMouseMovePos);
 | |
| 		}
 | |
| 	} else if (_photo && _photo->hasVideo()) {
 | |
| 		if (!_photoMedia->videoContent(Data::PhotoSize::Large).isEmpty()) {
 | |
| 			if (!QDir().exists(path)) {
 | |
| 				QDir().mkpath(path);
 | |
| 			}
 | |
| 			toName = filedialogDefaultName(u"photo"_q, u".mp4"_q, path);
 | |
| 			if (!_photoMedia->saveToFile(toName)) {
 | |
| 				toName = QString();
 | |
| 			}
 | |
| 		} else {
 | |
| 			_photo->loadVideo(Data::PhotoSize::Large, fileOrigin());
 | |
| 			_savePhotoVideoWhenLoaded = SavePhotoVideo::QuickSave;
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (!_photo || !_photoMedia->loaded()) {
 | |
| 			_saveVisible = contentCanBeSaved();
 | |
| 			update(_saveNav);
 | |
| 		} else {
 | |
| 			if (!QDir().exists(path)) {
 | |
| 				QDir().mkpath(path);
 | |
| 			}
 | |
| 			toName = filedialogDefaultName(u"photo"_q, u".jpg"_q, path);
 | |
| 			const auto saved = _photoMedia->saveToFile(toName);
 | |
| 			if (!saved) {
 | |
| 				toName = QString();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if (!toName.isEmpty()) {
 | |
| 		_saveMsgFilename = toName;
 | |
| 		const auto toIn = 1.;
 | |
| 		_saveMsgAnimation.start(
 | |
| 			[=](float64 value) {
 | |
| 				updateImage();
 | |
| 				if (value == toIn) {
 | |
| 					_saveMsgTimer.callOnce(st::mediaviewSaveMsgShown);
 | |
| 				}
 | |
| 			},
 | |
| 			0.,
 | |
| 			toIn,
 | |
| 			st::mediaviewSaveMsgShowing);
 | |
| 		updateImage();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::saveCancel() {
 | |
| 	if (_document && _document->loading()) {
 | |
| 		_document->cancel();
 | |
| 		if (_documentMedia->canBePlayed(_message)) {
 | |
| 			redisplayContent();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::showInFolder() {
 | |
| 	if (!_document) return;
 | |
| 
 | |
| 	auto filepath = _document->filepath(true);
 | |
| 	if (!filepath.isEmpty()) {
 | |
| 		File::ShowInFolder(filepath);
 | |
| 		close();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::forwardMedia() {
 | |
| 	if (!_session) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto &active = _session->windows();
 | |
| 	if (active.empty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto id = (_message && _message->allowsForward())
 | |
| 		? _message->fullId()
 | |
| 		: FullMsgId();
 | |
| 	if (id) {
 | |
| 		close();
 | |
| 		Window::ShowForwardMessagesBox(active.front(), { 1, id });
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::deleteMedia() {
 | |
| 	if (!_session) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto session = _session;
 | |
| 	const auto photo = _photo;
 | |
| 	const auto message = _message;
 | |
| 	const auto deletingPeerPhoto = [&] {
 | |
| 		if (!_message) {
 | |
| 			return true;
 | |
| 		} else if (_photo && _history) {
 | |
| 			if (_history->peer->userpicPhotoId() == _photo->id) {
 | |
| 				return _firstOpenedPeerPhoto;
 | |
| 			}
 | |
| 		}
 | |
| 		return false;
 | |
| 	}();
 | |
| 	close();
 | |
| 
 | |
| 	if (const auto window = findWindow()) {
 | |
| 		if (deletingPeerPhoto) {
 | |
| 			if (photo) {
 | |
| 				window->show(
 | |
| 					Ui::MakeConfirmBox({
 | |
| 						.text = tr::lng_delete_photo_sure(),
 | |
| 						.confirmed = crl::guard(_widget, [=] {
 | |
| 							session->api().peerPhoto().clear(photo);
 | |
| 							window->hideLayer();
 | |
| 						}),
 | |
| 						.confirmText = tr::lng_box_delete(),
 | |
| 					}),
 | |
| 					Ui::LayerOption::CloseOther);
 | |
| 			}
 | |
| 		} else if (message) {
 | |
| 			const auto suggestModerateActions = true;
 | |
| 			window->show(
 | |
| 				Box<DeleteMessagesBox>(message, suggestModerateActions),
 | |
| 				Ui::LayerOption::CloseOther);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::showMediaOverview() {
 | |
| 	if (_menu) {
 | |
| 		_menu->hideMenu(true);
 | |
| 	}
 | |
| 	update();
 | |
| 	if (const auto overviewType = computeOverviewType()) {
 | |
| 		close();
 | |
| 		if (SharedMediaOverviewType(*overviewType)) {
 | |
| 			if (const auto window = findWindow()) {
 | |
| 				const auto topic = _topicRootId
 | |
| 					? _history->peer->forumTopicFor(_topicRootId)
 | |
| 					: nullptr;
 | |
| 				if (_topicRootId && !topic) {
 | |
| 					return;
 | |
| 				}
 | |
| 				window->showSection(_topicRootId
 | |
| 					? std::make_shared<Info::Memento>(
 | |
| 						topic,
 | |
| 						Info::Section(*overviewType))
 | |
| 					: std::make_shared<Info::Memento>(
 | |
| 						_history->peer,
 | |
| 						Info::Section(*overviewType)));
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::copyMedia() {
 | |
| 	if (showCopyMediaRestriction()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow);
 | |
| 	if (_document) {
 | |
| 		QGuiApplication::clipboard()->setImage(transformedShownContent());
 | |
| 	} else if (_photo && _photoMedia->loaded()) {
 | |
| 		const auto image = _photoMedia->image(
 | |
| 			Data::PhotoSize::Large)->original();
 | |
| 		QGuiApplication::clipboard()->setImage(image);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::showAttachedStickers() {
 | |
| 	if (!_session) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto &active = _session->windows();
 | |
| 	if (active.empty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto window = active.front();
 | |
| 	auto &attachedStickers = _session->api().attachedStickers();
 | |
| 	if (_photo) {
 | |
| 		attachedStickers.requestAttachedStickerSets(window, _photo);
 | |
| 	} else if (_document) {
 | |
| 		attachedStickers.requestAttachedStickerSets(window, _document);
 | |
| 	} else {
 | |
| 		return;
 | |
| 	}
 | |
| 	close();
 | |
| }
 | |
| 
 | |
| auto OverlayWidget::sharedMediaType() const
 | |
| -> std::optional<SharedMediaType> {
 | |
| 	using Type = SharedMediaType;
 | |
| 	if (_message) {
 | |
| 		if (const auto media = _message->media()) {
 | |
| 			if (media->webpage()) {
 | |
| 				return std::nullopt;
 | |
| 			}
 | |
| 		}
 | |
| 		if (_photo) {
 | |
| 			if (_message->isService()) {
 | |
| 				return Type::ChatPhoto;
 | |
| 			}
 | |
| 			return Type::PhotoVideo;
 | |
| 		} else if (_document) {
 | |
| 			if (_document->isGifv()) {
 | |
| 				return Type::GIF;
 | |
| 			} else if (_document->isVideoFile()) {
 | |
| 				return Type::PhotoVideo;
 | |
| 			}
 | |
| 			return Type::File;
 | |
| 		}
 | |
| 	}
 | |
| 	return std::nullopt;
 | |
| }
 | |
| 
 | |
| auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> {
 | |
| 	if (!_message
 | |
| 		&& _peer
 | |
| 		&& !_user
 | |
| 		&& _photo
 | |
| 		&& _peer->userpicPhotoId() == _photo->id) {
 | |
| 		return SharedMediaKey{
 | |
| 			_history->peer->id,
 | |
| 			MsgId(0), // topicRootId
 | |
| 			_migrated ? _migrated->peer->id : 0,
 | |
| 			SharedMediaType::ChatPhoto,
 | |
| 			_photo
 | |
| 		};
 | |
| 	}
 | |
| 	if (!_message) {
 | |
| 		return std::nullopt;
 | |
| 	}
 | |
| 	const auto isScheduled = _message->isScheduled();
 | |
| 	const auto keyForType = [&](SharedMediaType type) -> SharedMediaKey {
 | |
| 		return {
 | |
| 			_history->peer->id,
 | |
| 			(isScheduled
 | |
| 				? SparseIdsMergedSlice::kScheduledTopicId
 | |
| 				: _topicRootId),
 | |
| 			_migrated ? _migrated->peer->id : 0,
 | |
| 			type,
 | |
| 			(_message->history() == _history
 | |
| 				? _message->id
 | |
| 				: (_message->id - ServerMaxMsgId))
 | |
| 		};
 | |
| 	};
 | |
| 	if (!_message->isRegular() && !isScheduled) {
 | |
| 		return std::nullopt;
 | |
| 	}
 | |
| 	return sharedMediaType() | keyForType;
 | |
| }
 | |
| 
 | |
| Data::FileOrigin OverlayWidget::fileOrigin() const {
 | |
| 	if (_message) {
 | |
| 		return _message->fullId();
 | |
| 	} else if (_photo && _user) {
 | |
| 		return Data::FileOriginUserPhoto(peerToUser(_user->id), _photo->id);
 | |
| 	} else if (_photo && _peer && _peer->userpicPhotoId() == _photo->id) {
 | |
| 		return Data::FileOriginPeerPhoto(_peer->id);
 | |
| 	}
 | |
| 	return Data::FileOrigin();
 | |
| }
 | |
| 
 | |
| Data::FileOrigin OverlayWidget::fileOrigin(const Entity &entity) const {
 | |
| 	if (const auto item = entity.item) {
 | |
| 		return item->fullId();
 | |
| 	} else if (!v::is<not_null<PhotoData*>>(entity.data)) {
 | |
| 		return Data::FileOrigin();
 | |
| 	}
 | |
| 	const auto photo = v::get<not_null<PhotoData*>>(entity.data);
 | |
| 	if (_user) {
 | |
| 		return Data::FileOriginUserPhoto(peerToUser(_user->id), photo->id);
 | |
| 	} else if (_peer && _peer->userpicPhotoId() == photo->id) {
 | |
| 		return Data::FileOriginPeerPhoto(_peer->id);
 | |
| 	}
 | |
| 	return Data::FileOrigin();
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::validSharedMedia() const {
 | |
| 	if (auto key = sharedMediaKey()) {
 | |
| 		if (!_sharedMedia) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		using Key = SharedMediaWithLastSlice::Key;
 | |
| 		auto inSameDomain = [](const Key &a, const Key &b) {
 | |
| 			return (a.type == b.type)
 | |
| 				&& (a.peerId == b.peerId)
 | |
| 				&& (a.topicRootId == b.topicRootId)
 | |
| 				&& (a.migratedPeerId == b.migratedPeerId);
 | |
| 		};
 | |
| 		auto countDistanceInData = [&](const Key &a, const Key &b) {
 | |
| 			return [&](const SharedMediaWithLastSlice &data) {
 | |
| 				return inSameDomain(a, b)
 | |
| 					? data.distance(a, b)
 | |
| 					: std::optional<int>();
 | |
| 			};
 | |
| 		};
 | |
| 
 | |
| 		if (key == _sharedMedia->key) {
 | |
| 			return true;
 | |
| 		} else if (!_sharedMediaDataKey
 | |
| 			|| _sharedMedia->key != *_sharedMediaDataKey) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		auto distance = _sharedMediaData
 | |
| 			| countDistanceInData(*key, _sharedMedia->key)
 | |
| 			| func::abs;
 | |
| 		if (distance) {
 | |
| 			return (*distance < kIdsPreloadAfter);
 | |
| 		}
 | |
| 	}
 | |
| 	return (_sharedMedia == nullptr);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::validateSharedMedia() {
 | |
| 	if (const auto key = sharedMediaKey()) {
 | |
| 		Assert(_history != nullptr);
 | |
| 
 | |
| 		_sharedMedia = std::make_unique<SharedMedia>(*key);
 | |
| 		auto viewer = (key->type == SharedMediaType::ChatPhoto)
 | |
| 			? SharedMediaWithLastReversedViewer
 | |
| 			: SharedMediaWithLastViewer;
 | |
| 		viewer(
 | |
| 			&_history->session(),
 | |
| 			*key,
 | |
| 			kIdsLimit,
 | |
| 			kIdsLimit
 | |
| 		) | rpl::start_with_next([this](
 | |
| 				SharedMediaWithLastSlice &&update) {
 | |
| 			handleSharedMediaUpdate(std::move(update));
 | |
| 		}, _sharedMedia->lifetime);
 | |
| 	} else {
 | |
| 		_sharedMedia = nullptr;
 | |
| 		_sharedMediaData = std::nullopt;
 | |
| 		_sharedMediaDataKey = std::nullopt;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleSharedMediaUpdate(SharedMediaWithLastSlice &&update) {
 | |
| 	if ((!_photo && !_document) || !_sharedMedia) {
 | |
| 		_sharedMediaData = std::nullopt;
 | |
| 		_sharedMediaDataKey = std::nullopt;
 | |
| 	} else {
 | |
| 		_sharedMediaData = std::move(update);
 | |
| 		_sharedMediaDataKey = _sharedMedia->key;
 | |
| 	}
 | |
| 	findCurrent();
 | |
| 	updateControls();
 | |
| 	preloadData(0);
 | |
| }
 | |
| 
 | |
| std::optional<OverlayWidget::UserPhotosKey> OverlayWidget::userPhotosKey() const {
 | |
| 	if (!_message && _user && _photo) {
 | |
| 		return UserPhotosKey{ peerToUser(_user->id), _photo->id };
 | |
| 	}
 | |
| 	return std::nullopt;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::validUserPhotos() const {
 | |
| 	if (const auto key = userPhotosKey()) {
 | |
| 		if (!_userPhotos) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		const auto countDistanceInData = [](const auto &a, const auto &b) {
 | |
| 			return [&](const UserPhotosSlice &data) {
 | |
| 				return data.distance(a, b);
 | |
| 			};
 | |
| 		};
 | |
| 
 | |
| 		const auto distance = (key == _userPhotos->key) ? 0 :
 | |
| 			_userPhotosData
 | |
| 			| countDistanceInData(*key, _userPhotos->key)
 | |
| 			| func::abs;
 | |
| 		if (distance) {
 | |
| 			return (*distance < kIdsPreloadAfter);
 | |
| 		}
 | |
| 	}
 | |
| 	return (_userPhotos == nullptr);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::validateUserPhotos() {
 | |
| 	if (const auto key = userPhotosKey()) {
 | |
| 		Assert(_user != nullptr);
 | |
| 
 | |
| 		_userPhotos = std::make_unique<UserPhotos>(*key);
 | |
| 		UserPhotosReversedViewer(
 | |
| 			&_user->session(),
 | |
| 			*key,
 | |
| 			kIdsLimit,
 | |
| 			kIdsLimit
 | |
| 		) | rpl::start_with_next([this](
 | |
| 				UserPhotosSlice &&update) {
 | |
| 			handleUserPhotosUpdate(std::move(update));
 | |
| 		}, _userPhotos->lifetime);
 | |
| 	} else {
 | |
| 		_userPhotos = nullptr;
 | |
| 		_userPhotosData = std::nullopt;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleUserPhotosUpdate(UserPhotosSlice &&update) {
 | |
| 	if (!_photo || !_userPhotos) {
 | |
| 		_userPhotosData = std::nullopt;
 | |
| 	} else {
 | |
| 		_userPhotosData = std::move(update);
 | |
| 	}
 | |
| 	findCurrent();
 | |
| 	updateControls();
 | |
| 	preloadData(0);
 | |
| }
 | |
| 
 | |
| std::optional<OverlayWidget::CollageKey> OverlayWidget::collageKey() const {
 | |
| 	if (_message) {
 | |
| 		if (const auto media = _message->media()) {
 | |
| 			if (const auto page = media->webpage()) {
 | |
| 				for (const auto &item : page->collage.items) {
 | |
| 					if (item == _photo || item == _document) {
 | |
| 						return item;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return std::nullopt;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::validCollage() const {
 | |
| 	if (const auto key = collageKey()) {
 | |
| 		if (!_collage) {
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		if (key == _collage->key) {
 | |
| 			return true;
 | |
| 		} else if (_collageData) {
 | |
| 			const auto &items = _collageData->items;
 | |
| 			if (ranges::find(items, *key) != end(items)
 | |
| 				&& ranges::find(items, _collage->key) != end(items)) {
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return (_collage == nullptr);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::validateCollage() {
 | |
| 	if (const auto key = collageKey()) {
 | |
| 		_collage = std::make_unique<Collage>(*key);
 | |
| 		_collageData = WebPageCollage();
 | |
| 		if (_message) {
 | |
| 			if (const auto media = _message->media()) {
 | |
| 				if (const auto page = media->webpage()) {
 | |
| 					_collageData = page->collage;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		_collage = nullptr;
 | |
| 		_collageData = std::nullopt;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshMediaViewer() {
 | |
| 	if (!validSharedMedia()) {
 | |
| 		validateSharedMedia();
 | |
| 	}
 | |
| 	if (!validUserPhotos()) {
 | |
| 		validateUserPhotos();
 | |
| 	}
 | |
| 	if (!validCollage()) {
 | |
| 		validateCollage();
 | |
| 	}
 | |
| 	findCurrent();
 | |
| 	updateControls();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshFromLabel() {
 | |
| 	if (_message) {
 | |
| 		_from = _message->senderOriginal();
 | |
| 		if (const auto info = _message->hiddenSenderInfo()) {
 | |
| 			_fromName = info->name;
 | |
| 		} else {
 | |
| 			Assert(_from != nullptr);
 | |
| 			const auto from = _from->migrateTo()
 | |
| 				? _from->migrateTo()
 | |
| 				: _from;
 | |
| 			_fromName = from->name();
 | |
| 		}
 | |
| 	} else {
 | |
| 		_from = _user;
 | |
| 		_fromName = _user ? _user->name() : QString();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshCaption() {
 | |
| 	_caption = Ui::Text::String();
 | |
| 	if (!_message) {
 | |
| 		return;
 | |
| 	} else if (const auto media = _message->media()) {
 | |
| 		if (media->webpage()) {
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	const auto caption = _message->originalText();
 | |
| 	if (caption.text.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	using namespace HistoryView;
 | |
| 	_caption = Ui::Text::String(st::msgMinWidth);
 | |
| 	const auto duration = (_streamed && _document)
 | |
| 		? DurationForTimestampLinks(_document)
 | |
| 		: 0;
 | |
| 	const auto base = duration
 | |
| 		? TimestampLinkBase(_document, _message->fullId())
 | |
| 		: QString();
 | |
| 	const auto captionRepaint = [=] {
 | |
| 		if (_fullScreenVideo || !_controlsOpacity.current()) {
 | |
| 			return;
 | |
| 		}
 | |
| 		update(captionGeometry());
 | |
| 	};
 | |
| 	const auto context = Core::MarkedTextContext{
 | |
| 		.session = &_message->history()->session(),
 | |
| 		.customEmojiRepaint = captionRepaint,
 | |
| 	};
 | |
| 	_caption.setMarkedText(
 | |
| 		st::mediaviewCaptionStyle,
 | |
| 		(base.isEmpty()
 | |
| 			? caption
 | |
| 			: AddTimestampLinks(caption, duration, base)),
 | |
| 		Ui::ItemTextOptions(_message),
 | |
| 		context);
 | |
| 	if (_caption.hasSpoilers()) {
 | |
| 		const auto weak = Ui::MakeWeak(widget());
 | |
| 		_caption.setSpoilerLinkFilter([=](const ClickContext &context) {
 | |
| 			return (weak != nullptr);
 | |
| 		});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshGroupThumbs() {
 | |
| 	const auto existed = (_groupThumbs != nullptr);
 | |
| 	if (_index && _sharedMediaData) {
 | |
| 		View::GroupThumbs::Refresh(
 | |
| 			_session,
 | |
| 			_groupThumbs,
 | |
| 			*_sharedMediaData,
 | |
| 			*_index,
 | |
| 			_groupThumbsAvailableWidth);
 | |
| 	} else if (_index && _userPhotosData) {
 | |
| 		View::GroupThumbs::Refresh(
 | |
| 			_session,
 | |
| 			_groupThumbs,
 | |
| 			*_userPhotosData,
 | |
| 			*_index,
 | |
| 			_groupThumbsAvailableWidth);
 | |
| 	} else if (_index && _collageData) {
 | |
| 		const auto messageId = _message ? _message->fullId() : FullMsgId();
 | |
| 		View::GroupThumbs::Refresh(
 | |
| 			_session,
 | |
| 			_groupThumbs,
 | |
| 			{ messageId, &*_collageData },
 | |
| 			*_index,
 | |
| 			_groupThumbsAvailableWidth);
 | |
| 	} else if (_groupThumbs) {
 | |
| 		_groupThumbs->clear();
 | |
| 		_groupThumbs->resizeToWidth(_groupThumbsAvailableWidth);
 | |
| 	}
 | |
| 	if (_groupThumbs && !existed) {
 | |
| 		initGroupThumbs();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::initGroupThumbs() {
 | |
| 	Expects(_groupThumbs != nullptr);
 | |
| 
 | |
| 	_groupThumbs->updateRequests(
 | |
| 	) | rpl::start_with_next([this](QRect rect) {
 | |
| 		const auto shift = (width() / 2);
 | |
| 		_groupThumbsRect = QRect(
 | |
| 			shift + rect.x(),
 | |
| 			_groupThumbsTop,
 | |
| 			rect.width(),
 | |
| 			_groupThumbs->height());
 | |
| 		update(_groupThumbsRect);
 | |
| 	}, _groupThumbs->lifetime());
 | |
| 
 | |
| 	_groupThumbs->activateRequests(
 | |
| 	) | rpl::start_with_next([this](View::GroupThumbs::Key key) {
 | |
| 		using CollageKey = View::GroupThumbs::CollageKey;
 | |
| 		if (const auto photoId = std::get_if<PhotoId>(&key)) {
 | |
| 			const auto photo = _session->data().photo(*photoId);
 | |
| 			moveToEntity({ photo, nullptr });
 | |
| 		} else if (const auto itemId = std::get_if<FullMsgId>(&key)) {
 | |
| 			moveToEntity(entityForItemId(*itemId));
 | |
| 		} else if (const auto collageKey = std::get_if<CollageKey>(&key)) {
 | |
| 			if (_collageData) {
 | |
| 				moveToEntity(entityForCollage(collageKey->index));
 | |
| 			}
 | |
| 		}
 | |
| 	}, _groupThumbs->lifetime());
 | |
| 
 | |
| 	_groupThumbsRect = QRect(
 | |
| 		_groupThumbsLeft,
 | |
| 		_groupThumbsTop,
 | |
| 		width() - 2 * _groupThumbsLeft,
 | |
| 		height() - _groupThumbsTop);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clearControlsState() {
 | |
| 	_saveMsgAnimation.stop();
 | |
| 	_saveMsgTimer.cancel();
 | |
| 	_loadRequest = 0;
 | |
| 	_over = _down = OverNone;
 | |
| 	_pressed = false;
 | |
| 	_dragging = 0;
 | |
| 	setCursor(style::cur_default);
 | |
| 	if (!_animations.empty()) {
 | |
| 		_animations.clear();
 | |
| 		_stateAnimation.stop();
 | |
| 	}
 | |
| 	if (!_animationOpacities.empty()) {
 | |
| 		_animationOpacities.clear();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| not_null<QWindow*> OverlayWidget::window() const {
 | |
| 	return _widget->windowHandle();
 | |
| }
 | |
| 
 | |
| int OverlayWidget::width() const {
 | |
| 	return _widget->width();
 | |
| }
 | |
| 
 | |
| int OverlayWidget::height() const {
 | |
| 	return _widget->height();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::update() {
 | |
| 	_widget->update();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::update(const QRegion ®ion) {
 | |
| 	_widget->update(region);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::isHidden() const {
 | |
| 	return _widget->isHidden();
 | |
| }
 | |
| 
 | |
| not_null<QWidget*> OverlayWidget::widget() const {
 | |
| 	return _widget;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::hide() {
 | |
| 	clearBeforeHide();
 | |
| 	applyHideWindowWorkaround();
 | |
| 	_widget->hide();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setCursor(style::cursor cursor) {
 | |
| 	_widget->setCursor(cursor);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setFocus() {
 | |
| 	_widget->setFocus();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::activate() {
 | |
| 	_widget->raise();
 | |
| 	_widget->activateWindow();
 | |
| 	QApplication::setActiveWindow(_widget);
 | |
| 	setFocus();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::show(OpenRequest request) {
 | |
| 	const auto document = request.document();
 | |
| 	const auto photo = request.photo();
 | |
| 	const auto contextItem = request.item();
 | |
| 	const auto contextPeer = request.peer();
 | |
| 	const auto contextTopicRootId = request.topicRootId();
 | |
| 	if (photo) {
 | |
| 		if (contextItem && contextPeer) {
 | |
| 			return;
 | |
| 		}
 | |
| 		setSession(&photo->session());
 | |
| 
 | |
| 		if (contextPeer) {
 | |
| 			setContext(contextPeer);
 | |
| 		} else if (contextItem) {
 | |
| 			setContext(ItemContext{ contextItem, contextTopicRootId });
 | |
| 		} else {
 | |
| 			setContext(v::null);
 | |
| 		}
 | |
| 
 | |
| 		clearControlsState();
 | |
| 		_firstOpenedPeerPhoto = (contextPeer != nullptr);
 | |
| 		assignMediaPointer(photo);
 | |
| 
 | |
| 		displayPhoto(photo);
 | |
| 		preloadData(0);
 | |
| 		activateControls();
 | |
| 	} else if (document) {
 | |
| 		setSession(&document->session());
 | |
| 
 | |
| 		if (contextItem) {
 | |
| 			setContext(ItemContext{ contextItem, contextTopicRootId });
 | |
| 		} else {
 | |
| 			setContext(v::null);
 | |
| 		}
 | |
| 
 | |
| 		clearControlsState();
 | |
| 
 | |
| 		_streamingStartPaused = false;
 | |
| 		displayDocument(
 | |
| 			document,
 | |
| 			request.cloudTheme()
 | |
| 				? *request.cloudTheme()
 | |
| 				: Data::CloudTheme(),
 | |
| 			{ request.continueStreaming(), request.startTime() });
 | |
| 		if (!isHidden()) {
 | |
| 			preloadData(0);
 | |
| 			activateControls();
 | |
| 		}
 | |
| 	}
 | |
| 	if (const auto controller = request.controller()) {
 | |
| 		_window = base::make_weak(&controller->window());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::displayPhoto(not_null<PhotoData*> photo) {
 | |
| 	if (photo->isNull()) {
 | |
| 		displayDocument(nullptr);
 | |
| 		return;
 | |
| 	}
 | |
| 	_touchbarDisplay.fire(TouchBarItemType::Photo);
 | |
| 
 | |
| 	clearStreaming();
 | |
| 	destroyThemePreview();
 | |
| 
 | |
| 	_fullScreenVideo = false;
 | |
| 	assignMediaPointer(photo);
 | |
| 	_rotation = _photo->owner().mediaRotation().get(_photo);
 | |
| 	_radial.stop();
 | |
| 
 | |
| 	refreshMediaViewer();
 | |
| 
 | |
| 	_staticContent = QImage();
 | |
| 	if (_photo->videoCanBePlayed()) {
 | |
| 		initStreaming();
 | |
| 	}
 | |
| 
 | |
| 	refreshCaption();
 | |
| 
 | |
| 	_blurred = true;
 | |
| 	_down = OverNone;
 | |
| 	if (!_staticContent.isNull()) {
 | |
| 		// Video thumbnail.
 | |
| 		const auto size = style::ConvertScale(
 | |
| 			flipSizeByRotation(_staticContent.size()));
 | |
| 		_w = size.width();
 | |
| 		_h = size.height();
 | |
| 	} else {
 | |
| 		const auto size = style::ConvertScale(flipSizeByRotation(QSize(
 | |
| 			photo->width(),
 | |
| 			photo->height())));
 | |
| 		_w = size.width();
 | |
| 		_h = size.height();
 | |
| 	}
 | |
| 	contentSizeChanged();
 | |
| 	refreshFromLabel();
 | |
| 	displayFinished();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::destroyThemePreview() {
 | |
| 	_themePreviewId = 0;
 | |
| 	_themePreviewShown = false;
 | |
| 	_themePreview.reset();
 | |
| 	_themeApply.destroy();
 | |
| 	_themeCancel.destroy();
 | |
| 	_themeShare.destroy();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::redisplayContent() {
 | |
| 	if (isHidden() || !_session) {
 | |
| 		return;
 | |
| 	} else if (_photo) {
 | |
| 		displayPhoto(_photo);
 | |
| 	} else {
 | |
| 		displayDocument(_document);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Empty messages shown as docs: doc can be nullptr.
 | |
| void OverlayWidget::displayDocument(
 | |
| 		DocumentData *doc,
 | |
| 		const Data::CloudTheme &cloud,
 | |
| 		const StartStreaming &startStreaming) {
 | |
| 	_fullScreenVideo = false;
 | |
| 	_staticContent = QImage();
 | |
| 	clearStreaming(_document != doc);
 | |
| 	destroyThemePreview();
 | |
| 	assignMediaPointer(doc);
 | |
| 
 | |
| 	_rotation = _document
 | |
| 		? _document->owner().mediaRotation().get(_document)
 | |
| 		: 0;
 | |
| 	_themeCloudData = cloud;
 | |
| 	_radial.stop();
 | |
| 
 | |
| 	_touchbarDisplay.fire(TouchBarItemType::None);
 | |
| 
 | |
| 	refreshMediaViewer();
 | |
| 	if (_document) {
 | |
| 		if (_document->sticker()) {
 | |
| 			if (const auto image = _documentMedia->getStickerLarge()) {
 | |
| 				setStaticContent(image->original());
 | |
| 			} else if (const auto thumbnail = _documentMedia->thumbnail()) {
 | |
| 				setStaticContent(thumbnail->pix(
 | |
| 					_document->dimensions,
 | |
| 					{ .options = Images::Option::Blur }
 | |
| 				).toImage());
 | |
| 			}
 | |
| 		} else {
 | |
| 			if (_documentMedia->canBePlayed(_message)
 | |
| 				&& initStreaming(startStreaming)) {
 | |
| 			} else if (_document->isVideoFile()) {
 | |
| 				_documentMedia->automaticLoad(fileOrigin(), _message);
 | |
| 				initStreamingThumbnail();
 | |
| 			} else if (_document->isTheme()) {
 | |
| 				_documentMedia->automaticLoad(fileOrigin(), _message);
 | |
| 				initThemePreview();
 | |
| 			} else {
 | |
| 				_documentMedia->automaticLoad(fileOrigin(), _message);
 | |
| 				_document->saveFromDataSilent();
 | |
| 				auto &location = _document->location(true);
 | |
| 				if (location.accessEnable()) {
 | |
| 					setStaticContent(PrepareStaticImage({
 | |
| 						.path = location.name(),
 | |
| 					}));
 | |
| 					if (!_staticContent.isNull()) {
 | |
| 						_touchbarDisplay.fire(TouchBarItemType::Photo);
 | |
| 					}
 | |
| 				} else {
 | |
| 					setStaticContent(PrepareStaticImage({
 | |
| 						.content = _documentMedia->bytes(),
 | |
| 					}));
 | |
| 					if (!_staticContent.isNull()) {
 | |
| 						_touchbarDisplay.fire(TouchBarItemType::Photo);
 | |
| 					}
 | |
| 				}
 | |
| 				location.accessDisable();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	refreshCaption();
 | |
| 
 | |
| 	const auto docGeneric = Layout::DocumentGenericPreview::Create(_document);
 | |
| 	_docExt = docGeneric.ext;
 | |
| 	_docIconColor = docGeneric.color;
 | |
| 	_docIcon = docGeneric.icon();
 | |
| 
 | |
| 	int32 extmaxw = (st::mediaviewFileIconSize - st::mediaviewFileExtPadding * 2);
 | |
| 	_docExtWidth = st::mediaviewFileExtFont->width(_docExt);
 | |
| 	if (_docExtWidth > extmaxw) {
 | |
| 		_docExt = st::mediaviewFileExtFont->elided(_docExt, extmaxw, Qt::ElideMiddle);
 | |
| 		_docExtWidth = st::mediaviewFileExtFont->width(_docExt);
 | |
| 	}
 | |
| 	if (documentBubbleShown()) {
 | |
| 		if (_document && _document->hasThumbnail()) {
 | |
| 			_document->loadThumbnail(fileOrigin());
 | |
| 			const auto tw = _documentMedia->thumbnailSize().width();
 | |
| 			const auto th = _documentMedia->thumbnailSize().height();
 | |
| 			if (!tw || !th) {
 | |
| 				_docThumbx = _docThumby = _docThumbw = 0;
 | |
| 			} else if (tw > th) {
 | |
| 				_docThumbw = (tw * st::mediaviewFileIconSize) / th;
 | |
| 				_docThumbx = (_docThumbw - st::mediaviewFileIconSize) / 2;
 | |
| 				_docThumby = 0;
 | |
| 			} else {
 | |
| 				_docThumbw = st::mediaviewFileIconSize;
 | |
| 				_docThumbx = 0;
 | |
| 				_docThumby = ((th * _docThumbw) / tw - st::mediaviewFileIconSize) / 2;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		int32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;
 | |
| 
 | |
| 		if (_document) {
 | |
| 			_docName = (_document->type == StickerDocument)
 | |
| 				? tr::lng_in_dlg_sticker(tr::now)
 | |
| 				: (_document->type == AnimatedDocument
 | |
| 					? u"GIF"_q
 | |
| 					: (_document->filename().isEmpty()
 | |
| 						? tr::lng_mediaview_doc_image(tr::now)
 | |
| 						: _document->filename()));
 | |
| 		} else {
 | |
| 			_docName = tr::lng_message_empty(tr::now);
 | |
| 		}
 | |
| 		_docNameWidth = st::mediaviewFileNameFont->width(_docName);
 | |
| 		if (_docNameWidth > maxw) {
 | |
| 			_docName = st::mediaviewFileNameFont->elided(_docName, maxw, Qt::ElideMiddle);
 | |
| 			_docNameWidth = st::mediaviewFileNameFont->width(_docName);
 | |
| 		}
 | |
| 	} else if (_themePreviewShown) {
 | |
| 		updateThemePreviewGeometry();
 | |
| 	} else if (!_staticContent.isNull()) {
 | |
| 		const auto size = style::ConvertScale(
 | |
| 			flipSizeByRotation(_staticContent.size()));
 | |
| 		_w = size.width();
 | |
| 		_h = size.height();
 | |
| 	} else if (videoShown()) {
 | |
| 		const auto contentSize = style::ConvertScale(videoSize());
 | |
| 		_w = contentSize.width();
 | |
| 		_h = contentSize.height();
 | |
| 	}
 | |
| 	contentSizeChanged();
 | |
| 	if (videoShown()) {
 | |
| 		applyVideoSize();
 | |
| 	}
 | |
| 	refreshFromLabel();
 | |
| 	_blurred = false;
 | |
| 	if (_showAsPip && _streamed && !videoIsGifOrUserpic()) {
 | |
| 		switchToPip();
 | |
| 	} else {
 | |
| 		displayFinished();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateThemePreviewGeometry() {
 | |
| 	if (_themePreviewShown) {
 | |
| 		auto previewRect = QRect((width() - st::themePreviewSize.width()) / 2, (height() - st::themePreviewSize.height()) / 2, st::themePreviewSize.width(), st::themePreviewSize.height());
 | |
| 		_themePreviewRect = previewRect.marginsAdded(st::themePreviewMargin);
 | |
| 		if (_themeApply) {
 | |
| 			auto right = qMax(width() - _themePreviewRect.x() - _themePreviewRect.width(), 0) + st::themePreviewMargin.right();
 | |
| 			auto bottom = qMin(height(), _themePreviewRect.y() + _themePreviewRect.height());
 | |
| 			_themeApply->moveToRight(right, bottom - st::themePreviewMargin.bottom() + (st::themePreviewMargin.bottom() - _themeApply->height()) / 2);
 | |
| 			right += _themeApply->width() + st::themePreviewButtonsSkip;
 | |
| 			_themeCancel->moveToRight(right, _themeApply->y());
 | |
| 			if (_themeShare) {
 | |
| 				_themeShare->moveToLeft(previewRect.x(), _themeApply->y());
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// For context menu event.
 | |
| 		_x = _themePreviewRect.x();
 | |
| 		_y = _themePreviewRect.y();
 | |
| 		_w = _themePreviewRect.width();
 | |
| 		_h = _themePreviewRect.height();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::displayFinished() {
 | |
| 	updateControls();
 | |
| 	if (isHidden()) {
 | |
| 		moveToScreen();
 | |
| 		//setAttribute(Qt::WA_DontShowOnScreen);
 | |
| 		//OverlayParent::setVisibleHook(true);
 | |
| 		//OverlayParent::setVisibleHook(false);
 | |
| 		//setAttribute(Qt::WA_DontShowOnScreen, false);
 | |
| 		Ui::Platform::UpdateOverlayed(_widget);
 | |
| 		if constexpr (!Platform::IsMac()) {
 | |
| 			_widget->showFullScreen();
 | |
| 		} else {
 | |
| 			_widget->show();
 | |
| 		}
 | |
| 		Ui::Platform::ShowOverAll(_widget);
 | |
| 		activate();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::canInitStreaming() const {
 | |
| 	return (_document && _documentMedia->canBePlayed(_message))
 | |
| 		|| (_photo && _photo->videoCanBePlayed());
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) {
 | |
| 	Expects(canInitStreaming());
 | |
| 
 | |
| 	if (_streamed) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	initStreamingThumbnail();
 | |
| 	if (!createStreamingObjects()) {
 | |
| 		if (_document) {
 | |
| 			_document->setInappPlaybackFailed();
 | |
| 		} else {
 | |
| 			_photo->setVideoPlaybackFailed();
 | |
| 		}
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	Core::App().updateNonIdle();
 | |
| 
 | |
| 	_streamed->instance.player().updates(
 | |
| 	) | rpl::start_with_next_error([=](Streaming::Update &&update) {
 | |
| 		handleStreamingUpdate(std::move(update));
 | |
| 	}, [=](Streaming::Error &&error) {
 | |
| 		handleStreamingError(std::move(error));
 | |
| 	}, _streamed->instance.lifetime());
 | |
| 
 | |
| 	if (startStreaming.continueStreaming) {
 | |
| 		_pip = nullptr;
 | |
| 	}
 | |
| 	if (!startStreaming.continueStreaming
 | |
| 		|| (!_streamed->instance.player().active()
 | |
| 			&& !_streamed->instance.player().finished())) {
 | |
| 		startStreamingPlayer(startStreaming);
 | |
| 	} else {
 | |
| 		updatePlaybackState();
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::startStreamingPlayer(
 | |
| 		const StartStreaming &startStreaming) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	const auto &player = _streamed->instance.player();
 | |
| 	if (player.playing()) {
 | |
| 		if (!_streamed->withSound) {
 | |
| 			return;
 | |
| 		}
 | |
| 		_pip = nullptr;
 | |
| 	} else if (!player.paused() && !player.finished() && !player.failed()) {
 | |
| 		_pip = nullptr;
 | |
| 	} else if (_pip && _streamed->withSound) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto position = _document
 | |
| 		? startStreaming.startTime
 | |
| 		: _photo
 | |
| 		? _photo->videoStartPosition()
 | |
| 		: 0;
 | |
| 	restartAtSeekPosition(position);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::initStreamingThumbnail() {
 | |
| 	Expects(_photo || _document);
 | |
| 
 | |
| 	_touchbarDisplay.fire(TouchBarItemType::Video);
 | |
| 
 | |
| 	auto userpicImage = std::optional<Image>();
 | |
| 	const auto computePhotoThumbnail = [&] {
 | |
| 		const auto thumbnail = _photoMedia->image(Data::PhotoSize::Thumbnail);
 | |
| 		if (thumbnail) {
 | |
| 			return thumbnail;
 | |
| 		} else if (_peer && _peer->userpicPhotoId() == _photo->id) {
 | |
| 			if (const auto view = _peer->activeUserpicView(); view.cloud) {
 | |
| 				if (!view.cloud->isNull()) {
 | |
| 					userpicImage.emplace(base::duplicate(*view.cloud));
 | |
| 					return &*userpicImage;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return thumbnail;
 | |
| 	};
 | |
| 	const auto good = _document
 | |
| 		? _documentMedia->goodThumbnail()
 | |
| 		: _photoMedia->image(Data::PhotoSize::Large);
 | |
| 	const auto thumbnail = _document
 | |
| 		? _documentMedia->thumbnail()
 | |
| 		: computePhotoThumbnail();
 | |
| 	const auto blurred = _document
 | |
| 		? _documentMedia->thumbnailInline()
 | |
| 		: _photoMedia->thumbnailInline();
 | |
| 	const auto size = _photo
 | |
| 		? QSize(
 | |
| 			_photo->videoLocation(Data::PhotoSize::Large).width(),
 | |
| 			_photo->videoLocation(Data::PhotoSize::Large).height())
 | |
| 		: good
 | |
| 		? good->size()
 | |
| 		: _document->dimensions;
 | |
| 	if (!good && !thumbnail && !blurred) {
 | |
| 		return;
 | |
| 	} else if (size.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto options = VideoThumbOptions(_document);
 | |
| 	const auto goodOptions = (options & ~Images::Option::Blur);
 | |
| 	setStaticContent((good
 | |
| 		? good
 | |
| 		: thumbnail
 | |
| 		? thumbnail
 | |
| 		: blurred
 | |
| 		? blurred
 | |
| 		: Image::BlankMedia().get())->pixNoCache(
 | |
| 			size,
 | |
| 			{
 | |
| 				.options = good ? goodOptions : options,
 | |
| 				.outer = size / style::DevicePixelRatio(),
 | |
| 			}
 | |
| 		).toImage());
 | |
| }
 | |
| 
 | |
| void OverlayWidget::streamingReady(Streaming::Information &&info) {
 | |
| 	if (videoShown()) {
 | |
| 		applyVideoSize();
 | |
| 	} else {
 | |
| 		updateContentRect();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::applyVideoSize() {
 | |
| 	const auto contentSize = style::ConvertScale(videoSize());
 | |
| 	if (contentSize != QSize(_width, _height)) {
 | |
| 		updateContentRect();
 | |
| 		_w = contentSize.width();
 | |
| 		_h = contentSize.height();
 | |
| 		contentSizeChanged();
 | |
| 	}
 | |
| 	updateContentRect();
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::createStreamingObjects() {
 | |
| 	Expects(_photo || _document);
 | |
| 
 | |
| 	if (_document) {
 | |
| 		_streamed = std::make_unique<Streamed>(
 | |
| 			_document,
 | |
| 			fileOrigin(),
 | |
| 			_widget,
 | |
| 			static_cast<PlaybackControls::Delegate*>(this),
 | |
| 			[=] { waitingAnimationCallback(); });
 | |
| 	} else {
 | |
| 		_streamed = std::make_unique<Streamed>(
 | |
| 			_photo,
 | |
| 			fileOrigin(),
 | |
| 			_widget,
 | |
| 			static_cast<PlaybackControls::Delegate*>(this),
 | |
| 			[=] { waitingAnimationCallback(); });
 | |
| 	}
 | |
| 	if (!_streamed->instance.valid()) {
 | |
| 		_streamed = nullptr;
 | |
| 		return false;
 | |
| 	}
 | |
| 	++_streamedCreated;
 | |
| 	_streamed->instance.setPriority(kOverlayLoaderPriority);
 | |
| 	_streamed->instance.lockPlayer();
 | |
| 	_streamed->withSound = _document
 | |
| 		&& (_document->isAudioFile()
 | |
| 			|| _document->isVideoFile()
 | |
| 			|| _document->isVoiceMessage()
 | |
| 			|| _document->isVideoMessage());
 | |
| 
 | |
| 	if (videoIsGifOrUserpic()) {
 | |
| 		_streamed->controls.hide();
 | |
| 	} else {
 | |
| 		refreshClipControllerGeometry();
 | |
| 		_streamed->controls.show();
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updatePowerSaveBlocker(
 | |
| 		const Player::TrackState &state) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	const auto block = (_document != nullptr)
 | |
| 		&& _document->isVideoFile()
 | |
| 		&& !IsPausedOrPausing(state.state)
 | |
| 		&& !IsStoppedOrStopping(state.state);
 | |
| 	base::UpdatePowerSaveBlocker(
 | |
| 		_streamed->powerSaveBlocker,
 | |
| 		block,
 | |
| 		base::PowerSaveBlockType::PreventDisplaySleep,
 | |
| 		[] { return u"Video playback is active"_q; },
 | |
| 		[=] { return window(); });
 | |
| }
 | |
| 
 | |
| QImage OverlayWidget::transformedShownContent() const {
 | |
| 	return transformShownContent(
 | |
| 		videoShown() ? currentVideoFrameImage() : _staticContent,
 | |
| 		finalContentRotation());
 | |
| }
 | |
| 
 | |
| QImage OverlayWidget::transformShownContent(
 | |
| 		QImage content,
 | |
| 		int rotation) const {
 | |
| 	if (rotation) {
 | |
| 		content = RotateFrameImage(std::move(content), rotation);
 | |
| 	}
 | |
| 	if (videoShown()) {
 | |
| 		const auto requiredSize = videoSize();
 | |
| 		if (content.size() != requiredSize) {
 | |
| 			content = content.scaled(
 | |
| 				requiredSize,
 | |
| 				Qt::IgnoreAspectRatio,
 | |
| 				Qt::SmoothTransformation);
 | |
| 		}
 | |
| 	}
 | |
| 	return content;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
 | |
| 	using namespace Streaming;
 | |
| 
 | |
| 	v::match(update.data, [&](Information &update) {
 | |
| 		streamingReady(std::move(update));
 | |
| 	}, [&](const PreloadedVideo &update) {
 | |
| 		updatePlaybackState();
 | |
| 	}, [&](const UpdateVideo &update) {
 | |
| 		updateContentRect();
 | |
| 		Core::App().updateNonIdle();
 | |
| 		updatePlaybackState();
 | |
| 	}, [&](const PreloadedAudio &update) {
 | |
| 		updatePlaybackState();
 | |
| 	}, [&](const UpdateAudio &update) {
 | |
| 		updatePlaybackState();
 | |
| 	}, [&](WaitingForData) {
 | |
| 	}, [&](MutedByOther) {
 | |
| 	}, [&](Finished) {
 | |
| 		updatePlaybackState();
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
 | |
| 	Expects(_document || _photo);
 | |
| 
 | |
| 	if (error == Streaming::Error::NotStreamable) {
 | |
| 		if (_document) {
 | |
| 			_document->setNotSupportsStreaming();
 | |
| 		} else {
 | |
| 			_photo->setVideoPlaybackFailed();
 | |
| 		}
 | |
| 	} else if (error == Streaming::Error::OpenFailed) {
 | |
| 		if (_document) {
 | |
| 			_document->setInappPlaybackFailed();
 | |
| 		} else {
 | |
| 			_photo->setVideoPlaybackFailed();
 | |
| 		}
 | |
| 	}
 | |
| 	if (canInitStreaming()) {
 | |
| 		updatePlaybackState();
 | |
| 	} else {
 | |
| 		redisplayContent();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::initThemePreview() {
 | |
| 	using namespace Window::Theme;
 | |
| 
 | |
| 	Assert(_document && _document->isTheme());
 | |
| 
 | |
| 	const auto bytes = _documentMedia->bytes();
 | |
| 	auto &location = _document->location();
 | |
| 	if (bytes.isEmpty()
 | |
| 		&& (location.isEmpty() || !location.accessEnable())) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_themePreviewShown = true;
 | |
| 
 | |
| 	auto current = CurrentData();
 | |
| 	current.backgroundId = Background()->id();
 | |
| 	current.backgroundImage = Background()->createCurrentImage();
 | |
| 	current.backgroundTiled = Background()->tile();
 | |
| 
 | |
| 	const auto &cloudList = _document->session().data().cloudThemes().list();
 | |
| 	const auto i = ranges::find(
 | |
| 		cloudList,
 | |
| 		_document->id,
 | |
| 		&Data::CloudTheme::documentId);
 | |
| 	const auto cloud = (i != end(cloudList)) ? *i : Data::CloudTheme();
 | |
| 	const auto isTrusted = (cloud.documentId != 0);
 | |
| 	const auto fields = [&] {
 | |
| 		auto result = _themeCloudData.id ? _themeCloudData : cloud;
 | |
| 		if (!result.documentId) {
 | |
| 			result.documentId = _document->id;
 | |
| 		}
 | |
| 		return result;
 | |
| 	}();
 | |
| 
 | |
| 	const auto weakSession = base::make_weak(&_document->session());
 | |
| 	const auto path = _document->location().name();
 | |
| 	const auto id = _themePreviewId = base::RandomValue<uint64>();
 | |
| 	const auto weak = Ui::MakeWeak(_widget);
 | |
| 	crl::async([=, data = std::move(current)]() mutable {
 | |
| 		auto preview = GeneratePreview(
 | |
| 			bytes,
 | |
| 			path,
 | |
| 			fields,
 | |
| 			std::move(data),
 | |
| 			Window::Theme::PreviewType::Extended);
 | |
| 		crl::on_main(weak, [=, result = std::move(preview)]() mutable {
 | |
| 			const auto session = weakSession.get();
 | |
| 			if (id != _themePreviewId || !session) {
 | |
| 				return;
 | |
| 			}
 | |
| 			_themePreviewId = 0;
 | |
| 			_themePreview = std::move(result);
 | |
| 			if (_themePreview) {
 | |
| 				_themeApply.create(
 | |
| 					_widget,
 | |
| 					tr::lng_theme_preview_apply(),
 | |
| 					st::themePreviewApplyButton);
 | |
| 				_themeApply->show();
 | |
| 				_themeApply->setClickedCallback([=] {
 | |
| 					const auto &object = Background()->themeObject();
 | |
| 					const auto currentlyIsCustom = !object.cloud.id
 | |
| 						&& !IsEmbeddedTheme(object.pathAbsolute);
 | |
| 					auto preview = std::move(_themePreview);
 | |
| 					close();
 | |
| 					Apply(std::move(preview));
 | |
| 					if (isTrusted && !currentlyIsCustom) {
 | |
| 						KeepApplied();
 | |
| 					}
 | |
| 				});
 | |
| 				_themeCancel.create(
 | |
| 					_widget,
 | |
| 					tr::lng_cancel(),
 | |
| 					st::themePreviewCancelButton);
 | |
| 				_themeCancel->show();
 | |
| 				_themeCancel->setClickedCallback([this] { close(); });
 | |
| 				if (const auto slug = _themeCloudData.slug; !slug.isEmpty()) {
 | |
| 					_themeShare.create(
 | |
| 						_widget,
 | |
| 						tr::lng_theme_share(),
 | |
| 						st::themePreviewCancelButton);
 | |
| 					_themeShare->show();
 | |
| 					_themeShare->setClickedCallback([=] {
 | |
| 						QGuiApplication::clipboard()->setText(
 | |
| 							session->createInternalLinkFull("addtheme/" + slug));
 | |
| 						Ui::Toast::Show(
 | |
| 							_widget,
 | |
| 							tr::lng_background_link_copied(tr::now));
 | |
| 					});
 | |
| 				} else {
 | |
| 					_themeShare.destroy();
 | |
| 				}
 | |
| 				updateControls();
 | |
| 			}
 | |
| 			update();
 | |
| 		});
 | |
| 	});
 | |
| 	location.accessDisable();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::refreshClipControllerGeometry() {
 | |
| 	if (!_streamed || videoIsGifOrUserpic()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (_groupThumbs && _groupThumbs->hiding()) {
 | |
| 		_groupThumbs = nullptr;
 | |
| 		_groupThumbsRect = QRect();
 | |
| 	}
 | |
| 	const auto controllerBottom = _groupThumbs
 | |
| 		? _groupThumbsTop
 | |
| 		: height();
 | |
| 	_streamed->controls.resize(st::mediaviewControllerSize);
 | |
| 	_streamed->controls.move(
 | |
| 		(width() - _streamed->controls.width()) / 2,
 | |
| 		controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height());
 | |
| 	Ui::SendPendingMoveResizeEvents(&_streamed->controls);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsPlay() {
 | |
| 	playbackPauseResume();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsPause() {
 | |
| 	playbackPauseResume();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsToFullScreen() {
 | |
| 	playbackToggleFullScreen();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsFromFullScreen() {
 | |
| 	playbackToggleFullScreen();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsToPictureInPicture() {
 | |
| 	if (!videoIsGifOrUserpic()) {
 | |
| 		switchToPip();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsRotate() {
 | |
| 	_oldGeometry = contentGeometry();
 | |
| 	_geometryAnimation.stop();
 | |
| 	if (_photo) {
 | |
| 		auto &storage = _photo->owner().mediaRotation();
 | |
| 		storage.set(_photo, storage.get(_photo) - 90);
 | |
| 		_rotation = storage.get(_photo);
 | |
| 		redisplayContent();
 | |
| 	} else if (_document) {
 | |
| 		auto &storage = _document->owner().mediaRotation();
 | |
| 		storage.set(_document, storage.get(_document) - 90);
 | |
| 		_rotation = storage.get(_document);
 | |
| 		if (videoShown()) {
 | |
| 			applyVideoSize();
 | |
| 		} else {
 | |
| 			redisplayContent();
 | |
| 		}
 | |
| 	}
 | |
| 	if (_opengl) {
 | |
| 		_geometryAnimation.start(
 | |
| 			[=] { update(); },
 | |
| 			0.,
 | |
| 			1.,
 | |
| 			st::widgetFadeDuration/*,
 | |
| 			st::easeOutCirc*/);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackPauseResume() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	_streamed->resumeOnCallEnd = false;
 | |
| 	if (_streamed->instance.player().failed()) {
 | |
| 		clearStreaming();
 | |
| 		if (!canInitStreaming() || !initStreaming()) {
 | |
| 			redisplayContent();
 | |
| 		}
 | |
| 	} else if (_streamed->instance.player().finished()
 | |
| 		|| !_streamed->instance.player().active()) {
 | |
| 		_streamingStartPaused = false;
 | |
| 		restartAtSeekPosition(0);
 | |
| 	} else if (_streamed->instance.player().paused()) {
 | |
| 		_streamed->instance.resume();
 | |
| 		updatePlaybackState();
 | |
| 		playbackPauseMusic();
 | |
| 	} else {
 | |
| 		_streamed->instance.pause();
 | |
| 		updatePlaybackState();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::seekRelativeTime(crl::time time) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	const auto newTime = std::clamp(
 | |
| 		_streamed->instance.info().video.state.position + time,
 | |
| 		crl::time(0),
 | |
| 		_streamed->instance.info().video.state.duration);
 | |
| 	restartAtSeekPosition(newTime);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::restartAtProgress(float64 progress) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	restartAtSeekPosition(_streamed->instance.info().video.state.duration
 | |
| 		* std::clamp(progress, 0., 1.));
 | |
| }
 | |
| 
 | |
| void OverlayWidget::restartAtSeekPosition(crl::time position) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (videoShown()) {
 | |
| 		_streamed->instance.saveFrameToCover();
 | |
| 		const auto saved = base::take(_rotation);
 | |
| 		setStaticContent(transformedShownContent());
 | |
| 		_rotation = saved;
 | |
| 		updateContentRect();
 | |
| 	}
 | |
| 	auto options = Streaming::PlaybackOptions();
 | |
| 	options.position = position;
 | |
| 	options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo();
 | |
| 	if (!_streamed->withSound) {
 | |
| 		options.mode = Streaming::Mode::Video;
 | |
| 		options.loop = true;
 | |
| 	} else {
 | |
| 		Assert(_document != nullptr);
 | |
| 		const auto messageId = _message ? _message->fullId() : FullMsgId();
 | |
| 		options.audioId = AudioMsgId(_document, messageId);
 | |
| 		options.speed = Core::App().settings().videoPlaybackSpeed();
 | |
| 		if (_pip) {
 | |
| 			_pip = nullptr;
 | |
| 		}
 | |
| 	}
 | |
| 	_streamed->instance.play(options);
 | |
| 	if (_streamingStartPaused) {
 | |
| 		_streamed->instance.pause();
 | |
| 	} else {
 | |
| 		playbackPauseMusic();
 | |
| 	}
 | |
| 	_streamed->pausedBySeek = false;
 | |
| 
 | |
| 	updatePlaybackState();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsSeekProgress(crl::time position) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (!_streamed->instance.player().paused()
 | |
| 		&& !_streamed->instance.player().finished()) {
 | |
| 		_streamed->pausedBySeek = true;
 | |
| 		playbackControlsPause();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsSeekFinished(crl::time position) {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	_streamingStartPaused = !_streamed->pausedBySeek
 | |
| 		&& !_streamed->instance.player().finished();
 | |
| 	restartAtSeekPosition(position);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsVolumeChanged(float64 volume) {
 | |
| 	if (_streamed) {
 | |
| 		Player::mixer()->setVideoVolume(volume);
 | |
| 	}
 | |
| 	Core::App().settings().setVideoVolume(volume);
 | |
| 	Core::App().saveSettingsDelayed();
 | |
| }
 | |
| 
 | |
| float64 OverlayWidget::playbackControlsCurrentVolume() {
 | |
| 	return Core::App().settings().videoVolume();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsVolumeToggled() {
 | |
| 	const auto volume = Core::App().settings().videoVolume();
 | |
| 	playbackControlsVolumeChanged(volume ? 0. : _lastPositiveVolume);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsVolumeChangeFinished() {
 | |
| 	const auto volume = Core::App().settings().videoVolume();
 | |
| 	if (volume > 0.) {
 | |
| 		_lastPositiveVolume = volume;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackControlsSpeedChanged(float64 speed) {
 | |
| 	DEBUG_LOG(("Media playback speed: change to %1.").arg(speed));
 | |
| 	if (_document) {
 | |
| 		DEBUG_LOG(("Media playback speed: %1 to settings.").arg(speed));
 | |
| 		Core::App().settings().setVideoPlaybackSpeed(speed);
 | |
| 		Core::App().saveSettingsDelayed();
 | |
| 	}
 | |
| 	if (_streamed && !videoIsGifOrUserpic()) {
 | |
| 		DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed));
 | |
| 		_streamed->instance.setSpeed(speed);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| float64 OverlayWidget::playbackControlsCurrentSpeed() {
 | |
| 	const auto result = Core::App().settings().videoPlaybackSpeed();
 | |
| 	DEBUG_LOG(("Media playback speed: now %1.").arg(result));
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::switchToPip() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 	Expects(_document != nullptr);
 | |
| 
 | |
| 	const auto document = _document;
 | |
| 	const auto messageId = _message ? _message->fullId() : FullMsgId();
 | |
| 	const auto topicRootId = _topicRootId;
 | |
| 	const auto closeAndContinue = [=] {
 | |
| 		_showAsPip = false;
 | |
| 		show(OpenRequest(
 | |
| 			findWindow(false),
 | |
| 			document,
 | |
| 			document->owner().message(messageId),
 | |
| 			topicRootId,
 | |
| 			true));
 | |
| 	};
 | |
| 	_showAsPip = true;
 | |
| 	_pip = std::make_unique<PipWrap>(
 | |
| 		_widget,
 | |
| 		document,
 | |
| 		_streamed->instance.shared(),
 | |
| 		closeAndContinue,
 | |
| 		[=] { _pip = nullptr; });
 | |
| 
 | |
| 	if (const auto raw = _message) {
 | |
| 		raw->history()->owner().itemRemoved(
 | |
| 		) | rpl::filter([=](not_null<const HistoryItem*> item) {
 | |
| 			return (raw == item);
 | |
| 		}) | rpl::start_with_next([=] {
 | |
| 			_pip = nullptr;
 | |
| 		}, _pip->lifetime);
 | |
| 	}
 | |
| 
 | |
| 	if (isHidden()) {
 | |
| 		clearBeforeHide();
 | |
| 		clearAfterHide();
 | |
| 	} else {
 | |
| 		close();
 | |
| 		if (const auto window = Core::App().activeWindow()) {
 | |
| 			window->activate();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackToggleFullScreen() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (!videoShown() || (videoIsGifOrUserpic() && !_fullScreenVideo)) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_fullScreenVideo = !_fullScreenVideo;
 | |
| 	if (_fullScreenVideo) {
 | |
| 		_fullScreenZoomCache = _zoom;
 | |
| 		setZoomLevel(kZoomToScreenLevel, true);
 | |
| 	} else {
 | |
| 		setZoomLevel(_fullScreenZoomCache, true);
 | |
| 		_streamed->controls.showAnimated();
 | |
| 	}
 | |
| 
 | |
| 	_streamed->controls.setInFullScreen(_fullScreenVideo);
 | |
| 	_touchbarFullscreenToggled.fire_copy(_fullScreenVideo);
 | |
| 	updateControls();
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackPauseOnCall() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (_streamed->instance.player().finished()
 | |
| 		|| _streamed->instance.player().paused()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_streamed->resumeOnCallEnd = true;
 | |
| 	_streamed->instance.pause();
 | |
| 	updatePlaybackState();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackResumeOnCall() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (_streamed->resumeOnCallEnd) {
 | |
| 		_streamed->resumeOnCallEnd = false;
 | |
| 		_streamed->instance.resume();
 | |
| 		updatePlaybackState();
 | |
| 		playbackPauseMusic();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::playbackPauseMusic() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (!_streamed->withSound) {
 | |
| 		return;
 | |
| 	}
 | |
| 	Player::instance()->pause(AudioMsgId::Type::Voice);
 | |
| 	Player::instance()->pause(AudioMsgId::Type::Song);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updatePlaybackState() {
 | |
| 	Expects(_streamed != nullptr);
 | |
| 
 | |
| 	if (videoIsGifOrUserpic()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto state = _streamed->instance.player().prepareLegacyState();
 | |
| 	if (state.position != kTimeUnknown && state.length != kTimeUnknown) {
 | |
| 		_streamed->controls.updatePlayback(state);
 | |
| 		updatePowerSaveBlocker(state);
 | |
| 		_touchbarTrackState.fire_copy(state);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::validatePhotoImage(Image *image, bool blurred) {
 | |
| 	if (!image) {
 | |
| 		return;
 | |
| 	} else if (!_staticContent.isNull() && (blurred || !_blurred)) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto use = flipSizeByRotation({ _width, _height })
 | |
| 		* cIntRetinaFactor();
 | |
| 	setStaticContent(image->pixNoCache(
 | |
| 		use,
 | |
| 		{ .options = (blurred ? Images::Option::Blur : Images::Option()) }
 | |
| 	).toImage());
 | |
| 	_blurred = blurred;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::validatePhotoCurrentImage() {
 | |
| 	if (!_photo) {
 | |
| 		return;
 | |
| 	}
 | |
| 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Large), false);
 | |
| 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Thumbnail), true);
 | |
| 	validatePhotoImage(_photoMedia->image(Data::PhotoSize::Small), true);
 | |
| 	validatePhotoImage(_photoMedia->thumbnailInline(), true);
 | |
| 	if (_staticContent.isNull()
 | |
| 		&& !_message
 | |
| 		&& _peer
 | |
| 		&& _peer->hasUserpic()) {
 | |
| 		if (const auto view = _peer->activeUserpicView(); view.cloud) {
 | |
| 			if (!view.cloud->isNull()) {
 | |
| 				auto image = Image(base::duplicate(*view.cloud));
 | |
| 				validatePhotoImage(&image, true);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if (_staticContent.isNull()) {
 | |
| 		_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| Ui::GL::ChosenRenderer OverlayWidget::chooseRenderer(
 | |
| 		Ui::GL::Capabilities capabilities) {
 | |
| 	const auto use = Platform::IsMac()
 | |
| 		? true
 | |
| 		: capabilities.transparency;
 | |
| 	LOG(("OpenGL: %1 (OverlayWidget)").arg(Logs::b(use)));
 | |
| 	if (use) {
 | |
| 		_opengl = true;
 | |
| 		return {
 | |
| 			.renderer = std::make_unique<RendererGL>(this),
 | |
| 			.backend = Ui::GL::Backend::OpenGL,
 | |
| 		};
 | |
| 	}
 | |
| 	return {
 | |
| 		.renderer = std::make_unique<RendererSW>(this),
 | |
| 		.backend = Ui::GL::Backend::Raster,
 | |
| 	};
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paint(not_null<Renderer*> renderer) {
 | |
| 	renderer->paintBackground();
 | |
| 	if (contentShown()) {
 | |
| 		if (videoShown()) {
 | |
| 			renderer->paintTransformedVideoFrame(contentGeometry());
 | |
| 			if (_streamed->instance.player().ready()) {
 | |
| 				_streamed->instance.markFrameShown();
 | |
| 			}
 | |
| 		} else {
 | |
| 			validatePhotoCurrentImage();
 | |
| 			const auto fillTransparentBackground = (!_document
 | |
| 				|| (!_document->sticker() && !_document->isVideoMessage()))
 | |
| 				&& _staticContentTransparent;
 | |
| 			renderer->paintTransformedStaticContent(
 | |
| 				_staticContent,
 | |
| 				contentGeometry(),
 | |
| 				_staticContentTransparent,
 | |
| 				fillTransparentBackground);
 | |
| 		}
 | |
| 		paintRadialLoading(renderer);
 | |
| 	} else {
 | |
| 		if (_themePreviewShown) {
 | |
| 			renderer->paintThemePreview(_themePreviewRect);
 | |
| 		} else if (documentBubbleShown() && !_docRect.isEmpty()) {
 | |
| 			renderer->paintDocumentBubble(_docRect, _docIconRect);
 | |
| 		}
 | |
| 	}
 | |
| 	if (isSaveMsgShown()) {
 | |
| 		renderer->paintSaveMsg(_saveMsg);
 | |
| 	}
 | |
| 
 | |
| 	const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current();
 | |
| 	if (opacity > 0) {
 | |
| 		paintControls(renderer, opacity);
 | |
| 		renderer->paintFooter(footerGeometry(), opacity);
 | |
| 		if (!_caption.isEmpty()) {
 | |
| 			renderer->paintCaption(captionGeometry(), opacity);
 | |
| 		}
 | |
| 		if (_groupThumbs) {
 | |
| 			renderer->paintGroupThumbs(
 | |
| 				QRect(
 | |
| 					_groupThumbsLeft,
 | |
| 					_groupThumbsTop,
 | |
| 					width() - 2 * _groupThumbsLeft,
 | |
| 					_groupThumbs->height()),
 | |
| 				opacity);
 | |
| 		}
 | |
| 	}
 | |
| 	checkGroupThumbsAnimation();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::checkGroupThumbsAnimation() {
 | |
| 	if (_groupThumbs
 | |
| 		&& (!_streamed || _streamed->instance.player().ready())) {
 | |
| 		_groupThumbs->checkForAnimationStart();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintRadialLoading(not_null<Renderer*> renderer) {
 | |
| 	const auto radial = _radial.animating();
 | |
| 	if (_streamed) {
 | |
| 		if (!_streamed->instance.waitingShown()) {
 | |
| 			return;
 | |
| 		}
 | |
| 	} else if (!radial && (!_document || _documentMedia->loaded())) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto radialOpacity = radial ? _radial.opacity() : 0.;
 | |
| 	const auto inner = radialRect();
 | |
| 	Assert(!inner.isEmpty());
 | |
| 
 | |
| 	renderer->paintRadialLoading(inner, radial, radialOpacity);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintRadialLoadingContent(
 | |
| 		Painter &p,
 | |
| 		QRect inner,
 | |
| 		bool radial,
 | |
| 		float64 radialOpacity) const {
 | |
| 	const auto arc = inner.marginsRemoved(QMargins(
 | |
| 		st::radialLine,
 | |
| 		st::radialLine,
 | |
| 		st::radialLine,
 | |
| 		st::radialLine));
 | |
| 	const auto paintBg = [&](float64 opacity, QBrush brush) {
 | |
| 		p.setOpacity(opacity);
 | |
| 		p.setPen(Qt::NoPen);
 | |
| 		p.setBrush(brush);
 | |
| 		{
 | |
| 			PainterHighQualityEnabler hq(p);
 | |
| 			p.drawEllipse(inner);
 | |
| 		}
 | |
| 		p.setOpacity(1.);
 | |
| 	};
 | |
| 
 | |
| 	if (_streamed) {
 | |
| 		paintBg(
 | |
| 			_streamed->instance.waitingOpacity(),
 | |
| 			st::radialBg);
 | |
| 		Ui::InfiniteRadialAnimation::Draw(
 | |
| 			p,
 | |
| 			_streamed->instance.waitingState(),
 | |
| 			arc.topLeft(),
 | |
| 			arc.size(),
 | |
| 			width(),
 | |
| 			st::radialFg,
 | |
| 			st::radialLine);
 | |
| 		return;
 | |
| 	}
 | |
| 	if (_photo) {
 | |
| 		paintBg(radialOpacity, st::radialBg);
 | |
| 	} else {
 | |
| 		const auto o = overLevel(OverIcon);
 | |
| 		paintBg(
 | |
| 			_documentMedia->loaded() ? radialOpacity : 1.,
 | |
| 			anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, o));
 | |
| 
 | |
| 		const auto icon = [&]() -> const style::icon * {
 | |
| 			if (radial || _document->loading()) {
 | |
| 				return &st::historyFileThumbCancel;
 | |
| 			}
 | |
| 			return &st::historyFileThumbDownload;
 | |
| 		}();
 | |
| 		if (icon) {
 | |
| 			icon->paintInCenter(p, inner);
 | |
| 		}
 | |
| 	}
 | |
| 	if (radial) {
 | |
| 		p.setOpacity(1);
 | |
| 		_radial.draw(p, arc, st::radialLine, st::radialFg);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintThemePreviewContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect clip) {
 | |
| 	const auto fill = outer.intersected(clip);
 | |
| 	if (!fill.isEmpty()) {
 | |
| 		if (_themePreview) {
 | |
| 			p.drawImage(
 | |
| 				outer.topLeft(),
 | |
| 				_themePreview->preview);
 | |
| 		} else {
 | |
| 			p.fillRect(fill, st::themePreviewBg);
 | |
| 			p.setFont(st::themePreviewLoadingFont);
 | |
| 			p.setPen(st::themePreviewLoadingFg);
 | |
| 			p.drawText(
 | |
| 				outer,
 | |
| 				(_themePreviewId
 | |
| 					? tr::lng_theme_preview_generating(tr::now)
 | |
| 					: tr::lng_theme_preview_invalid(tr::now)),
 | |
| 				QTextOption(style::al_center));
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	const auto fillOverlay = [&](QRect fill) {
 | |
| 		const auto clipped = fill.intersected(clip);
 | |
| 		if (!clipped.isEmpty()) {
 | |
| 			p.setOpacity(st::themePreviewOverlayOpacity);
 | |
| 			p.fillRect(clipped, st::themePreviewBg);
 | |
| 			p.setOpacity(1.);
 | |
| 		}
 | |
| 	};
 | |
| 	auto titleRect = QRect(
 | |
| 		outer.x(),
 | |
| 		outer.y(),
 | |
| 		outer.width(),
 | |
| 		st::themePreviewMargin.top());
 | |
| 	if (titleRect.x() < 0) {
 | |
| 		titleRect = QRect(
 | |
| 			0,
 | |
| 			outer.y(),
 | |
| 			width(),
 | |
| 			st::themePreviewMargin.top());
 | |
| 	}
 | |
| 	if (titleRect.y() < 0) {
 | |
| 		titleRect.moveTop(0);
 | |
| 		fillOverlay(titleRect);
 | |
| 	}
 | |
| 	titleRect = titleRect.marginsRemoved(QMargins(
 | |
| 		st::themePreviewMargin.left(),
 | |
| 		st::themePreviewTitleTop,
 | |
| 		st::themePreviewMargin.right(),
 | |
| 		(titleRect.height()
 | |
| 			- st::themePreviewTitleTop
 | |
| 			- st::themePreviewTitleFont->height)));
 | |
| 	if (titleRect.intersects(clip)) {
 | |
| 		p.setFont(st::themePreviewTitleFont);
 | |
| 		p.setPen(st::themePreviewTitleFg);
 | |
| 		const auto title = _themeCloudData.title.isEmpty()
 | |
| 			? tr::lng_theme_preview_title(tr::now)
 | |
| 			: _themeCloudData.title;
 | |
| 		const auto elided = st::themePreviewTitleFont->elided(
 | |
| 			title,
 | |
| 			titleRect.width());
 | |
| 		p.drawTextLeft(titleRect.x(), titleRect.y(), width(), elided);
 | |
| 	}
 | |
| 
 | |
| 	auto buttonsRect = QRect(
 | |
| 		outer.x(),
 | |
| 		outer.y() + outer.height() - st::themePreviewMargin.bottom(),
 | |
| 		outer.width(),
 | |
| 		st::themePreviewMargin.bottom());
 | |
| 	if (buttonsRect.y() + buttonsRect.height() > height()) {
 | |
| 		buttonsRect.moveTop(height() - buttonsRect.height());
 | |
| 		fillOverlay(buttonsRect);
 | |
| 	}
 | |
| 	if (_themeShare && _themeCloudData.usersCount > 0) {
 | |
| 		p.setFont(st::boxTextFont);
 | |
| 		p.setPen(st::windowSubTextFg);
 | |
| 		const auto left = outer.x()
 | |
| 			+ (_themeShare->x() - _themePreviewRect.x())
 | |
| 			+ _themeShare->width()
 | |
| 			- (st::themePreviewCancelButton.width / 2);
 | |
| 		const auto baseline = outer.y()
 | |
| 			+ (_themeShare->y() - _themePreviewRect.y())
 | |
| 			+ st::themePreviewCancelButton.padding.top()
 | |
| 			+ st::themePreviewCancelButton.textTop
 | |
| 			+ st::themePreviewCancelButton.font->ascent;
 | |
| 		p.drawText(
 | |
| 			left,
 | |
| 			baseline,
 | |
| 			tr::lng_theme_preview_users(
 | |
| 				tr::now,
 | |
| 				lt_count,
 | |
| 				_themeCloudData.usersCount));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintDocumentBubbleContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect icon,
 | |
| 		QRect clip) const {
 | |
| 	p.fillRect(outer, st::mediaviewFileBg);
 | |
| 	if (icon.intersects(clip)) {
 | |
| 		if (!_document || !_document->hasThumbnail()) {
 | |
| 			p.fillRect(icon, _docIconColor);
 | |
| 			const auto radial = _radial.animating();
 | |
| 			const auto radialOpacity = radial ? _radial.opacity() : 0.;
 | |
| 			if ((!_document || _documentMedia->loaded()) && (!radial || radialOpacity < 1) && _docIcon) {
 | |
| 				_docIcon->paint(p, icon.x() + (icon.width() - _docIcon->width()), icon.y(), width());
 | |
| 				p.setPen(st::mediaviewFileExtFg);
 | |
| 				p.setFont(st::mediaviewFileExtFont);
 | |
| 				if (!_docExt.isEmpty()) {
 | |
| 					p.drawText(icon.x() + (icon.width() - _docExtWidth) / 2, icon.y() + st::mediaviewFileExtTop + st::mediaviewFileExtFont->ascent, _docExt);
 | |
| 				}
 | |
| 			}
 | |
| 		} else if (const auto thumbnail = _documentMedia->thumbnail()) {
 | |
| 			int32 rf(cIntRetinaFactor());
 | |
| 			p.drawPixmap(icon.topLeft(), thumbnail->pix(_docThumbw), QRect(_docThumbx * rf, _docThumby * rf, st::mediaviewFileIconSize * rf, st::mediaviewFileIconSize * rf));
 | |
| 		}
 | |
| 	}
 | |
| 	if (!icon.contains(clip)) {
 | |
| 		p.setPen(st::mediaviewFileNameFg);
 | |
| 		p.setFont(st::mediaviewFileNameFont);
 | |
| 		p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileNameTop, width(), _docName, _docNameWidth);
 | |
| 
 | |
| 		p.setPen(st::mediaviewFileSizeFg);
 | |
| 		p.setFont(st::mediaviewFont);
 | |
| 		p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileSizeTop, width(), _docSize, _docSizeWidth);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintSaveMsgContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect clip) {
 | |
| 	p.setOpacity(_saveMsgAnimation.value(1.));
 | |
| 	Ui::FillRoundRect(p, outer, st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners);
 | |
| 	st::mediaviewSaveMsgCheck.paint(p, outer.topLeft() + st::mediaviewSaveMsgCheckPos, width());
 | |
| 
 | |
| 	p.setPen(st::mediaviewSaveMsgFg);
 | |
| 	_saveMsgText.draw(p, {
 | |
| 		.position = QPoint(
 | |
| 			outer.x() + st::mediaviewSaveMsgPadding.left(),
 | |
| 			outer.y() + st::mediaviewSaveMsgPadding.top()),
 | |
| 		.availableWidth = outer.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right(),
 | |
| 		.palette = &st::mediaviewTextPalette,
 | |
| 	});
 | |
| 	p.setOpacity(1);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintControls(
 | |
| 		not_null<Renderer*> renderer,
 | |
| 		float64 opacity) {
 | |
| 	struct Control {
 | |
| 		OverState state = OverNone;
 | |
| 		bool visible = false;
 | |
| 		const QRect &outer;
 | |
| 		const QRect &inner;
 | |
| 		const style::icon &icon;
 | |
| 	};
 | |
| 	const QRect kEmpty;
 | |
| 	// When adding / removing controls please update RendererGL.
 | |
| 	const Control controls[] = {
 | |
| 		{
 | |
| 			OverLeftNav,
 | |
| 			_leftNavVisible,
 | |
| 			_leftNav,
 | |
| 			_leftNavIcon,
 | |
| 			st::mediaviewLeft },
 | |
| 		{
 | |
| 			OverRightNav,
 | |
| 			_rightNavVisible,
 | |
| 			_rightNav,
 | |
| 			_rightNavIcon,
 | |
| 			st::mediaviewRight },
 | |
| 		{
 | |
| 			OverClose,
 | |
| 			true,
 | |
| 			_closeNav,
 | |
| 			_closeNavIcon,
 | |
| 			st::mediaviewClose },
 | |
| 		{
 | |
| 			OverSave,
 | |
| 			_saveVisible,
 | |
| 			kEmpty,
 | |
| 			_saveNavIcon,
 | |
| 			st::mediaviewSave },
 | |
| 		{
 | |
| 			OverRotate,
 | |
| 			_rotateVisible,
 | |
| 			kEmpty,
 | |
| 			_rotateNavIcon,
 | |
| 			st::mediaviewRotate },
 | |
| 		{
 | |
| 			OverMore,
 | |
| 			true,
 | |
| 			kEmpty,
 | |
| 			_moreNavIcon,
 | |
| 			st::mediaviewMore },
 | |
| 	};
 | |
| 
 | |
| 	renderer->paintControlsStart();
 | |
| 	for (const auto &control : controls) {
 | |
| 		if (!control.visible) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		const auto bg = overLevel(control.state);
 | |
| 		const auto icon = bg * st::mediaviewIconOverOpacity
 | |
| 			+ (1 - bg) * st::mediaviewIconOpacity;
 | |
| 		renderer->paintControl(
 | |
| 			control.state,
 | |
| 			control.outer,
 | |
| 			bg * opacity,
 | |
| 			control.inner,
 | |
| 			icon * opacity,
 | |
| 			control.icon);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintFooterContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect clip,
 | |
| 		float64 opacity) {
 | |
| 	p.setPen(st::mediaviewControlFg);
 | |
| 	p.setFont(st::mediaviewThickFont);
 | |
| 
 | |
| 	// header
 | |
| 	const auto shift = outer.topLeft() - _headerNav.topLeft();
 | |
| 	const auto header = _headerNav.translated(shift);
 | |
| 	const auto name = _nameNav.translated(shift);
 | |
| 	const auto date = _dateNav.translated(shift);
 | |
| 	if (header.intersects(clip)) {
 | |
| 		auto o = _headerHasLink ? overLevel(OverHeader) : 0;
 | |
| 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
 | |
| 		p.drawText(header.left(), header.top() + st::mediaviewThickFont->ascent, _headerText);
 | |
| 
 | |
| 		if (o > 0) {
 | |
| 			p.setOpacity(o * opacity);
 | |
| 			p.drawLine(header.left(), header.top() + st::mediaviewThickFont->ascent + 1, header.right(), header.top() + st::mediaviewThickFont->ascent + 1);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	p.setFont(st::mediaviewFont);
 | |
| 
 | |
| 	// name
 | |
| 	if (_nameNav.isValid() && name.intersects(clip)) {
 | |
| 		float64 o = _from ? overLevel(OverName) : 0.;
 | |
| 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
 | |
| 		_fromNameLabel.drawElided(p, name.left(), name.top(), name.width());
 | |
| 
 | |
| 		if (o > 0) {
 | |
| 			p.setOpacity(o * opacity);
 | |
| 			p.drawLine(name.left(), name.top() + st::mediaviewFont->ascent + 1, name.right(), name.top() + st::mediaviewFont->ascent + 1);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// date
 | |
| 	if (date.intersects(clip)) {
 | |
| 		float64 o = overLevel(OverDate);
 | |
| 		p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity);
 | |
| 		p.drawText(date.left(), date.top() + st::mediaviewFont->ascent, _dateText);
 | |
| 
 | |
| 		if (o > 0) {
 | |
| 			p.setOpacity(o * opacity);
 | |
| 			p.drawLine(date.left(), date.top() + st::mediaviewFont->ascent + 1, date.right(), date.top() + st::mediaviewFont->ascent + 1);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QRect OverlayWidget::footerGeometry() const {
 | |
| 	return _headerNav.united(_nameNav).united(_dateNav);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintCaptionContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect clip,
 | |
| 		float64 opacity) {
 | |
| 	const auto inner = outer.marginsRemoved(st::mediaviewCaptionPadding);
 | |
| 	p.setOpacity(opacity);
 | |
| 	p.setBrush(st::mediaviewCaptionBg);
 | |
| 	p.setPen(Qt::NoPen);
 | |
| 	p.drawRoundedRect(outer, st::mediaviewCaptionRadius, st::mediaviewCaptionRadius);
 | |
| 	if (inner.intersects(clip)) {
 | |
| 		p.setPen(st::mediaviewCaptionFg);
 | |
| 		_caption.draw(p, {
 | |
| 			.position = inner.topLeft(),
 | |
| 			.availableWidth = inner.width(),
 | |
| 			.palette = &st::mediaviewTextPalette,
 | |
| 			.spoiler = Ui::Text::DefaultSpoilerCache(),
 | |
| 			.elisionLines = inner.height() / st::mediaviewCaptionStyle.font->height,
 | |
| 		});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QRect OverlayWidget::captionGeometry() const {
 | |
| 	return _captionRect.marginsAdded(st::mediaviewCaptionPadding);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::paintGroupThumbsContent(
 | |
| 		Painter &p,
 | |
| 		QRect outer,
 | |
| 		QRect clip,
 | |
| 		float64 opacity) {
 | |
| 	p.setOpacity(opacity);
 | |
| 	_groupThumbs->paint(p, outer.x(), outer.y(), width());
 | |
| 	if (_groupThumbs->hidden()) {
 | |
| 		_groupThumbs = nullptr;
 | |
| 		_groupThumbsRect = QRect();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::isSaveMsgShown() const {
 | |
| 	return _saveMsgAnimation.animating() || _saveMsgTimer.isActive();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {
 | |
| 	const auto key = e->key();
 | |
| 	const auto modifiers = e->modifiers();
 | |
| 	const auto ctrl = modifiers.testFlag(Qt::ControlModifier);
 | |
| 	if (_streamed) {
 | |
| 		// Ctrl + F for full screen toggle is in eventFilter().
 | |
| 		const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl)
 | |
| 			&& (key == Qt::Key_Enter || key == Qt::Key_Return);
 | |
| 		if (toggleFull) {
 | |
| 			playbackToggleFullScreen();
 | |
| 			return;
 | |
| 		} else if (key == Qt::Key_Space) {
 | |
| 			playbackPauseResume();
 | |
| 			return;
 | |
| 		} else if (_fullScreenVideo) {
 | |
| 			if (key == Qt::Key_Escape) {
 | |
| 				playbackToggleFullScreen();
 | |
| 			} else if (key == Qt::Key_0) {
 | |
| 				activateControls();
 | |
| 				restartAtSeekPosition(0);
 | |
| 			} else if (key >= Qt::Key_1 && key <= Qt::Key_9) {
 | |
| 				activateControls();
 | |
| 				const auto index = int(key - Qt::Key_0);
 | |
| 				restartAtProgress(index / 10.0);
 | |
| 			} else if (key == Qt::Key_Left) {
 | |
| 				activateControls();
 | |
| 				seekRelativeTime(-kSeekTimeMs);
 | |
| 			} else if (key == Qt::Key_Right) {
 | |
| 				activateControls();
 | |
| 				seekRelativeTime(kSeekTimeMs);
 | |
| 			}
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	if (!_menu && key == Qt::Key_Escape) {
 | |
| 		if (_document && _document->loading() && !_streamed) {
 | |
| 			handleDocumentClick();
 | |
| 		} else {
 | |
| 			close();
 | |
| 		}
 | |
| 	} else if (e == QKeySequence::Save || e == QKeySequence::SaveAs) {
 | |
| 		saveAs();
 | |
| 	} else if (key == Qt::Key_Copy || (key == Qt::Key_C && ctrl)) {
 | |
| 		copyMedia();
 | |
| 	} else if (key == Qt::Key_Enter
 | |
| 		|| key == Qt::Key_Return
 | |
| 		|| key == Qt::Key_Space) {
 | |
| 		if (_streamed) {
 | |
| 			playbackPauseResume();
 | |
| 		} else if (_document
 | |
| 			&& !_document->loading()
 | |
| 			&& (documentBubbleShown() || !_documentMedia->loaded())) {
 | |
| 			handleDocumentClick();
 | |
| 		}
 | |
| 	} else if (key == Qt::Key_Left) {
 | |
| 		if (_controlsHideTimer.isActive()) {
 | |
| 			activateControls();
 | |
| 		}
 | |
| 		moveToNext(-1);
 | |
| 	} else if (key == Qt::Key_Right) {
 | |
| 		if (_controlsHideTimer.isActive()) {
 | |
| 			activateControls();
 | |
| 		}
 | |
| 		moveToNext(1);
 | |
| 	} else if (ctrl) {
 | |
| 		if (key == Qt::Key_Plus
 | |
| 			|| key == Qt::Key_Equal
 | |
| 			|| key == Qt::Key_Asterisk
 | |
| 			|| key == ']') {
 | |
| 			zoomIn();
 | |
| 		} else if (key == Qt::Key_Minus || key == Qt::Key_Underscore) {
 | |
| 			zoomOut();
 | |
| 		} else if (key == Qt::Key_0) {
 | |
| 			zoomReset();
 | |
| 		} else if (key == Qt::Key_I) {
 | |
| 			update();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleWheelEvent(not_null<QWheelEvent*> e) {
 | |
| 	constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
 | |
| 
 | |
| 	_verticalWheelDelta += e->angleDelta().y();
 | |
| 	while (qAbs(_verticalWheelDelta) >= step) {
 | |
| 		if (_verticalWheelDelta < 0) {
 | |
| 			_verticalWheelDelta += step;
 | |
| 			if (e->modifiers().testFlag(Qt::ControlModifier)) {
 | |
| 				zoomOut();
 | |
| 			} else if (e->source() == Qt::MouseEventNotSynthesized) {
 | |
| 				moveToNext(1);
 | |
| 			}
 | |
| 		} else {
 | |
| 			_verticalWheelDelta -= step;
 | |
| 			if (e->modifiers().testFlag(Qt::ControlModifier)) {
 | |
| 				zoomIn();
 | |
| 			} else if (e->source() == Qt::MouseEventNotSynthesized) {
 | |
| 				moveToNext(-1);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setZoomLevel(int newZoom, bool force) {
 | |
| 	if (!force && _zoom == newZoom) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault;
 | |
| 	float64 nx, ny, z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
 | |
| 	const auto contentSize = videoShown()
 | |
| 		? style::ConvertScale(videoSize())
 | |
| 		: QSize(_width, _height);
 | |
| 	_oldGeometry = contentGeometry();
 | |
| 	_geometryAnimation.stop();
 | |
| 
 | |
| 	_w = contentSize.width();
 | |
| 	_h = contentSize.height();
 | |
| 	if (z >= 0) {
 | |
| 		nx = (_x - width() / 2.) / (z + 1);
 | |
| 		ny = (_y - height() / 2.) / (z + 1);
 | |
| 	} else {
 | |
| 		nx = (_x - width() / 2.) * (-z + 1);
 | |
| 		ny = (_y - height() / 2.) * (-z + 1);
 | |
| 	}
 | |
| 	_zoom = newZoom;
 | |
| 	z = (_zoom == kZoomToScreenLevel) ? full : _zoom;
 | |
| 	if (z > 0) {
 | |
| 		_w = qRound(_w * (z + 1));
 | |
| 		_h = qRound(_h * (z + 1));
 | |
| 		_x = qRound(nx * (z + 1) + width() / 2.);
 | |
| 		_y = qRound(ny * (z + 1) + height() / 2.);
 | |
| 	} else {
 | |
| 		_w = qRound(_w / (-z + 1));
 | |
| 		_h = qRound(_h / (-z + 1));
 | |
| 		_x = qRound(nx / (-z + 1) + width() / 2.);
 | |
| 		_y = qRound(ny / (-z + 1) + height() / 2.);
 | |
| 	}
 | |
| 	snapXY();
 | |
| 	if (_opengl) {
 | |
| 		_geometryAnimation.start(
 | |
| 			[=] { update(); },
 | |
| 			0.,
 | |
| 			1.,
 | |
| 			st::widgetFadeDuration/*,
 | |
| 			anim::easeOutCirc*/);
 | |
| 	}
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| OverlayWidget::Entity OverlayWidget::entityForUserPhotos(int index) const {
 | |
| 	Expects(_userPhotosData.has_value());
 | |
| 	Expects(_session != nullptr);
 | |
| 
 | |
| 	if (index < 0 || index >= _userPhotosData->size()) {
 | |
| 		return { v::null, nullptr };
 | |
| 	}
 | |
| 	const auto id = (*_userPhotosData)[index];
 | |
| 	if (const auto photo = _session->data().photo(id)) {
 | |
| 		return { photo, nullptr };
 | |
| 	}
 | |
| 	return { v::null, nullptr };
 | |
| }
 | |
| 
 | |
| OverlayWidget::Entity OverlayWidget::entityForSharedMedia(int index) const {
 | |
| 	Expects(_sharedMediaData.has_value());
 | |
| 
 | |
| 	if (index < 0 || index >= _sharedMediaData->size()) {
 | |
| 		return { v::null, nullptr };
 | |
| 	}
 | |
| 	auto value = (*_sharedMediaData)[index];
 | |
| 	if (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {
 | |
| 		// Last peer photo.
 | |
| 		return { *photo, nullptr };
 | |
| 	} else if (const auto itemId = std::get_if<FullMsgId>(&value)) {
 | |
| 		return entityForItemId(*itemId);
 | |
| 	}
 | |
| 	return { v::null, nullptr };
 | |
| }
 | |
| 
 | |
| OverlayWidget::Entity OverlayWidget::entityForCollage(int index) const {
 | |
| 	Expects(_collageData.has_value());
 | |
| 	Expects(_session != nullptr);
 | |
| 
 | |
| 	const auto &items = _collageData->items;
 | |
| 	if (!_message || index < 0 || index >= items.size()) {
 | |
| 		return { v::null, nullptr };
 | |
| 	}
 | |
| 	if (const auto document = std::get_if<DocumentData*>(&items[index])) {
 | |
| 		return { *document, _message, _topicRootId };
 | |
| 	} else if (const auto photo = std::get_if<PhotoData*>(&items[index])) {
 | |
| 		return { *photo, _message, _topicRootId };
 | |
| 	}
 | |
| 	return { v::null, nullptr };
 | |
| }
 | |
| 
 | |
| OverlayWidget::Entity OverlayWidget::entityForItemId(const FullMsgId &itemId) const {
 | |
| 	Expects(_session != nullptr);
 | |
| 
 | |
| 	if (const auto item = _session->data().message(itemId)) {
 | |
| 		if (const auto media = item->media()) {
 | |
| 			if (const auto photo = media->photo()) {
 | |
| 				return { photo, item, _topicRootId };
 | |
| 			} else if (const auto document = media->document()) {
 | |
| 				return { document, item, _topicRootId };
 | |
| 			}
 | |
| 		}
 | |
| 		return { v::null, item, _topicRootId };
 | |
| 	}
 | |
| 	return { v::null, nullptr };
 | |
| }
 | |
| 
 | |
| OverlayWidget::Entity OverlayWidget::entityByIndex(int index) const {
 | |
| 	if (_sharedMediaData) {
 | |
| 		return entityForSharedMedia(index);
 | |
| 	} else if (_userPhotosData) {
 | |
| 		return entityForUserPhotos(index);
 | |
| 	} else if (_collageData) {
 | |
| 		return entityForCollage(index);
 | |
| 	}
 | |
| 	return { v::null, nullptr };
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setContext(
 | |
| 	std::variant<
 | |
| 		v::null_t,
 | |
| 		ItemContext,
 | |
| 		not_null<PeerData*>> context) {
 | |
| 	if (const auto item = std::get_if<ItemContext>(&context)) {
 | |
| 		_message = item->item;
 | |
| 		_history = _message->history();
 | |
| 		_peer = _history->peer;
 | |
| 		_topicRootId = _peer->isForum() ? item->topicRootId : MsgId();
 | |
| 	} else if (const auto peer = std::get_if<not_null<PeerData*>>(&context)) {
 | |
| 		_peer = *peer;
 | |
| 		_history = _peer->owner().history(_peer);
 | |
| 		_message = nullptr;
 | |
| 		_topicRootId = MsgId();
 | |
| 	} else {
 | |
| 		_message = nullptr;
 | |
| 		_topicRootId = MsgId();
 | |
| 		_history = nullptr;
 | |
| 		_peer = nullptr;
 | |
| 	}
 | |
| 	_migrated = nullptr;
 | |
| 	if (_history) {
 | |
| 		if (_history->peer->migrateFrom()) {
 | |
| 			_migrated = _history->owner().history(
 | |
| 				_history->peer->migrateFrom());
 | |
| 		} else if (_history->peer->migrateTo()) {
 | |
| 			_migrated = _history;
 | |
| 			_history = _history->owner().history(_history->peer->migrateTo());
 | |
| 		}
 | |
| 	}
 | |
| 	_user = _peer ? _peer->asUser() : nullptr;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::setSession(not_null<Main::Session*> session) {
 | |
| 	if (_session == session) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	clearSession();
 | |
| 	_session = session;
 | |
| 	_widget->setWindowIcon(Window::CreateIcon(session));
 | |
| 
 | |
| 	session->downloaderTaskFinished(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		if (!isHidden()) {
 | |
| 			updateControls();
 | |
| 			checkForSaveLoaded();
 | |
| 		}
 | |
| 	}, _sessionLifetime);
 | |
| 
 | |
| 	session->data().documentLoadProgress(
 | |
| 	) | rpl::filter([=] {
 | |
| 		return !isHidden();
 | |
| 	}) | rpl::start_with_next([=](not_null<DocumentData*> document) {
 | |
| 		documentUpdated(document);
 | |
| 	}, _sessionLifetime);
 | |
| 
 | |
| 	session->data().itemIdChanged(
 | |
| 	) | rpl::start_with_next([=](const Data::Session::IdChange &change) {
 | |
| 		changingMsgId(change.newId, change.oldId);
 | |
| 	}, _sessionLifetime);
 | |
| 
 | |
| 	session->data().itemRemoved(
 | |
| 	) | rpl::filter([=](not_null<const HistoryItem*> item) {
 | |
| 		return (_message == item);
 | |
| 	}) | rpl::start_with_next([=] {
 | |
| 		close();
 | |
| 		clearSession();
 | |
| 	}, _sessionLifetime);
 | |
| 
 | |
| 	session->account().sessionChanges(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		clearSession();
 | |
| 	}, _sessionLifetime);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::moveToNext(int delta) {
 | |
| 	if (!_index) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	auto newIndex = *_index + delta;
 | |
| 	return moveToEntity(entityByIndex(newIndex), delta);
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::moveToEntity(const Entity &entity, int preloadDelta) {
 | |
| 	if (v::is_null(entity.data) && !entity.item) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	if (const auto item = entity.item) {
 | |
| 		setContext(ItemContext{ item, entity.topicRootId });
 | |
| 	} else if (_peer) {
 | |
| 		setContext(_peer);
 | |
| 	} else {
 | |
| 		setContext(v::null);
 | |
| 	}
 | |
| 	clearStreaming();
 | |
| 	_streamingStartPaused = false;
 | |
| 	if (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {
 | |
| 		displayPhoto(*photo);
 | |
| 	} else if (auto document = std::get_if<not_null<DocumentData*>>(&entity.data)) {
 | |
| 		displayDocument(*document);
 | |
| 	} else {
 | |
| 		displayDocument(nullptr);
 | |
| 	}
 | |
| 	preloadData(preloadDelta);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::preloadData(int delta) {
 | |
| 	if (!_index) {
 | |
| 		return;
 | |
| 	}
 | |
| 	auto from = *_index + (delta ? -delta : -1);
 | |
| 	auto till = *_index + (delta ? delta * kPreloadCount : 1);
 | |
| 	if (from > till) std::swap(from, till);
 | |
| 
 | |
| 	auto photos = base::flat_set<std::shared_ptr<Data::PhotoMedia>>();
 | |
| 	auto documents = base::flat_set<std::shared_ptr<Data::DocumentMedia>>();
 | |
| 	for (auto index = from; index != till + 1; ++index) {
 | |
| 		auto entity = entityByIndex(index);
 | |
| 		if (auto photo = std::get_if<not_null<PhotoData*>>(&entity.data)) {
 | |
| 			const auto [i, ok] = photos.emplace((*photo)->createMediaView());
 | |
| 			(*i)->wanted(Data::PhotoSize::Small, fileOrigin(entity));
 | |
| 			(*photo)->load(fileOrigin(entity), LoadFromCloudOrLocal, true);
 | |
| 		} else if (auto document = std::get_if<not_null<DocumentData*>>(
 | |
| 				&entity.data)) {
 | |
| 			const auto [i, ok] = documents.emplace(
 | |
| 				(*document)->createMediaView());
 | |
| 			(*i)->thumbnailWanted(fileOrigin(entity));
 | |
| 			if (!(*i)->canBePlayed(entity.item)) {
 | |
| 				(*i)->automaticLoad(fileOrigin(entity), entity.item);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	_preloadPhotos = std::move(photos);
 | |
| 	_preloadDocuments = std::move(documents);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleMousePress(
 | |
| 		QPoint position,
 | |
| 		Qt::MouseButton button) {
 | |
| 	updateOver(position);
 | |
| 	if (_menu || !_receiveMouse) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	ClickHandler::pressed();
 | |
| 
 | |
| 	if (button == Qt::LeftButton) {
 | |
| 		_down = OverNone;
 | |
| 		if (!ClickHandler::getPressed()) {
 | |
| 			if (_over == OverLeftNav && moveToNext(-1)) {
 | |
| 				_lastAction = position;
 | |
| 			} else if (_over == OverRightNav && moveToNext(1)) {
 | |
| 				_lastAction = position;
 | |
| 			} else if (_over == OverName
 | |
| 				|| _over == OverDate
 | |
| 				|| _over == OverHeader
 | |
| 				|| _over == OverSave
 | |
| 				|| _over == OverRotate
 | |
| 				|| _over == OverIcon
 | |
| 				|| _over == OverMore
 | |
| 				|| _over == OverClose
 | |
| 				|| _over == OverVideo) {
 | |
| 				_down = _over;
 | |
| 			} else if (!_saveMsg.contains(position) || !isSaveMsgShown()) {
 | |
| 				_pressed = true;
 | |
| 				_dragging = 0;
 | |
| 				updateCursor();
 | |
| 				_mStart = position;
 | |
| 				_xStart = _x;
 | |
| 				_yStart = _y;
 | |
| 			}
 | |
| 		}
 | |
| 	} else if (button == Qt::MiddleButton) {
 | |
| 		zoomReset();
 | |
| 	}
 | |
| 	activateControls();
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::handleDoubleClick(
 | |
| 		QPoint position,
 | |
| 		Qt::MouseButton button) {
 | |
| 	updateOver(position);
 | |
| 
 | |
| 	if (_over != OverVideo || !_streamed || button != Qt::LeftButton) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	playbackToggleFullScreen();
 | |
| 	playbackPauseResume();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::snapXY() {
 | |
| 	int32 xmin = width() - _w, xmax = 0;
 | |
| 	int32 ymin = height() - _h, ymax = 0;
 | |
| 	if (xmin > (width() - _w) / 2) xmin = (width() - _w) / 2;
 | |
| 	if (xmax < (width() - _w) / 2) xmax = (width() - _w) / 2;
 | |
| 	if (ymin > (height() - _h) / 2) ymin = (height() - _h) / 2;
 | |
| 	if (ymax < (height() - _h) / 2) ymax = (height() - _h) / 2;
 | |
| 	if (_x < xmin) _x = xmin;
 | |
| 	if (_x > xmax) _x = xmax;
 | |
| 	if (_y < ymin) _y = ymin;
 | |
| 	if (_y > ymax) _y = ymax;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleMouseMove(QPoint position) {
 | |
| 	updateOver(position);
 | |
| 	if (_lastAction.x() >= 0
 | |
| 		&& ((position - _lastAction).manhattanLength()
 | |
| 			>= st::mediaviewDeltaFromLastAction)) {
 | |
| 		_lastAction = QPoint(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction);
 | |
| 	}
 | |
| 	if (_pressed) {
 | |
| 		if (!_dragging
 | |
| 			&& ((position - _mStart).manhattanLength()
 | |
| 				>= QApplication::startDragDistance())) {
 | |
| 			_dragging = QRect(_x, _y, _w, _h).contains(_mStart) ? 1 : -1;
 | |
| 			if (_dragging > 0) {
 | |
| 				if (_w > width() || _h > height()) {
 | |
| 					setCursor(style::cur_sizeall);
 | |
| 				} else {
 | |
| 					setCursor(style::cur_default);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		if (_dragging > 0) {
 | |
| 			_x = _xStart + (position - _mStart).x();
 | |
| 			_y = _yStart + (position - _mStart).y();
 | |
| 			snapXY();
 | |
| 			update();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateOverRect(OverState state) {
 | |
| 	switch (state) {
 | |
| 	case OverLeftNav: update(_leftNav); break;
 | |
| 	case OverRightNav: update(_rightNav); break;
 | |
| 	case OverName: update(_nameNav); break;
 | |
| 	case OverDate: update(_dateNav); break;
 | |
| 	case OverSave: update(_saveNavIcon); break;
 | |
| 	case OverRotate: update(_rotateNavIcon); break;
 | |
| 	case OverIcon: update(_docIconRect); break;
 | |
| 	case OverHeader: update(_headerNav); break;
 | |
| 	case OverClose: update(_closeNav); break;
 | |
| 	case OverMore: update(_moreNavIcon); break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::updateOverState(OverState newState) {
 | |
| 	bool result = true;
 | |
| 	if (_over != newState) {
 | |
| 		if (newState == OverMore && !_ignoringDropdown) {
 | |
| 			_dropdownShowTimer.callOnce(0);
 | |
| 		} else {
 | |
| 			_dropdownShowTimer.cancel();
 | |
| 		}
 | |
| 		updateOverRect(_over);
 | |
| 		updateOverRect(newState);
 | |
| 		if (_over != OverNone) {
 | |
| 			_animations[_over] = crl::now();
 | |
| 			const auto i = _animationOpacities.find(_over);
 | |
| 			if (i != end(_animationOpacities)) {
 | |
| 				i->second.start(0);
 | |
| 			} else {
 | |
| 				_animationOpacities.emplace(_over, anim::value(1, 0));
 | |
| 			}
 | |
| 			if (!_stateAnimation.animating()) {
 | |
| 				_stateAnimation.start();
 | |
| 			}
 | |
| 		} else {
 | |
| 			result = false;
 | |
| 		}
 | |
| 		_over = newState;
 | |
| 		if (newState != OverNone) {
 | |
| 			_animations[_over] = crl::now();
 | |
| 			const auto i = _animationOpacities.find(_over);
 | |
| 			if (i != end(_animationOpacities)) {
 | |
| 				i->second.start(1);
 | |
| 			} else {
 | |
| 				_animationOpacities.emplace(_over, anim::value(0, 1));
 | |
| 			}
 | |
| 			if (!_stateAnimation.animating()) {
 | |
| 				_stateAnimation.start();
 | |
| 			}
 | |
| 		}
 | |
| 		updateCursor();
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateOver(QPoint pos) {
 | |
| 	ClickHandlerPtr lnk;
 | |
| 	ClickHandlerHost *lnkhost = nullptr;
 | |
| 	if (isSaveMsgShown() && _saveMsg.contains(pos)) {
 | |
| 		auto textState = _saveMsgText.getState(pos - _saveMsg.topLeft() - QPoint(st::mediaviewSaveMsgPadding.left(), st::mediaviewSaveMsgPadding.top()), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right());
 | |
| 		lnk = textState.link;
 | |
| 		lnkhost = this;
 | |
| 	} else if (_captionRect.contains(pos)) {
 | |
| 		auto textState = _caption.getState(pos - _captionRect.topLeft(), _captionRect.width());
 | |
| 		lnk = textState.link;
 | |
| 		lnkhost = this;
 | |
| 	} else if (_groupThumbs && _groupThumbsRect.contains(pos)) {
 | |
| 		const auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop);
 | |
| 		lnk = _groupThumbs->getState(point);
 | |
| 		lnkhost = this;
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	// retina
 | |
| 	if (pos.x() == width()) {
 | |
| 		pos.setX(pos.x() - 1);
 | |
| 	}
 | |
| 	if (pos.y() == height()) {
 | |
| 		pos.setY(pos.y() - 1);
 | |
| 	}
 | |
| 
 | |
| 	ClickHandler::setActive(lnk, lnkhost);
 | |
| 
 | |
| 	if (_pressed || _dragging) return;
 | |
| 
 | |
| 	if (_fullScreenVideo) {
 | |
| 		updateOverState(OverVideo);
 | |
| 	} else if (_leftNavVisible && _leftNav.contains(pos)) {
 | |
| 		updateOverState(OverLeftNav);
 | |
| 	} else if (_rightNavVisible && _rightNav.contains(pos)) {
 | |
| 		updateOverState(OverRightNav);
 | |
| 	} else if (_from && _nameNav.contains(pos)) {
 | |
| 		updateOverState(OverName);
 | |
| 	} else if (_message && _message->isRegular() && _dateNav.contains(pos)) {
 | |
| 		updateOverState(OverDate);
 | |
| 	} else if (_headerHasLink && _headerNav.contains(pos)) {
 | |
| 		updateOverState(OverHeader);
 | |
| 	} else if (_saveVisible && _saveNav.contains(pos)) {
 | |
| 		updateOverState(OverSave);
 | |
| 	} else if (_rotateVisible && _rotateNav.contains(pos)) {
 | |
| 		updateOverState(OverRotate);
 | |
| 	} else if (_document && documentBubbleShown() && _docIconRect.contains(pos)) {
 | |
| 		updateOverState(OverIcon);
 | |
| 	} else if (_moreNav.contains(pos)) {
 | |
| 		updateOverState(OverMore);
 | |
| 	} else if (_closeNav.contains(pos)) {
 | |
| 		updateOverState(OverClose);
 | |
| 	} else if (documentContentShown() && finalContentRect().contains(pos)) {
 | |
| 		if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) {
 | |
| 			updateOverState(OverVideo);
 | |
| 		} else if (!_streamed && !_documentMedia->loaded()) {
 | |
| 			updateOverState(OverIcon);
 | |
| 		} else if (_over != OverNone) {
 | |
| 			updateOverState(OverNone);
 | |
| 		}
 | |
| 	} else if (_over != OverNone) {
 | |
| 		updateOverState(OverNone);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleMouseRelease(
 | |
| 		QPoint position,
 | |
| 		Qt::MouseButton button) {
 | |
| 	updateOver(position);
 | |
| 
 | |
| 	if (const auto activated = ClickHandler::unpressed()) {
 | |
| 		if (activated->dragText() == u"internal:show_saved_message"_q) {
 | |
| 			showSaveMsgFile();
 | |
| 			return;
 | |
| 		}
 | |
| 		// There may be a mention / hashtag / bot command link.
 | |
| 		// For now activate account for all activated links.
 | |
| 		// findWindow() will activate account.
 | |
| 		ActivateClickHandler(_widget, activated, {
 | |
| 			button,
 | |
| 			QVariant::fromValue(ClickHandlerContext{
 | |
| 				.itemId = _message ? _message->fullId() : FullMsgId(),
 | |
| 				.sessionWindow = base::make_weak(findWindow()),
 | |
| 			})
 | |
| 		});
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (_over == OverName && _down == OverName) {
 | |
| 		if (_from) {
 | |
| 			close();
 | |
| 			if (const auto window = findWindow(true)) {
 | |
| 				window->showPeerInfo(_from);
 | |
| 			}
 | |
| 		}
 | |
| 	} else if (_over == OverDate && _down == OverDate) {
 | |
| 		toMessage();
 | |
| 	} else if (_over == OverHeader && _down == OverHeader) {
 | |
| 		showMediaOverview();
 | |
| 	} else if (_over == OverSave && _down == OverSave) {
 | |
| 		downloadMedia();
 | |
| 	} else if (_over == OverRotate && _down == OverRotate) {
 | |
| 		playbackControlsRotate();
 | |
| 	} else if (_over == OverIcon && _down == OverIcon) {
 | |
| 		handleDocumentClick();
 | |
| 	} else if (_over == OverMore && _down == OverMore) {
 | |
| 		InvokeQueued(_widget, [=] { showDropdown(); });
 | |
| 	} else if (_over == OverClose && _down == OverClose) {
 | |
| 		close();
 | |
| 	} else if (_over == OverVideo && _down == OverVideo) {
 | |
| 		if (_streamed) {
 | |
| 			playbackPauseResume();
 | |
| 		}
 | |
| 	} else if (_pressed) {
 | |
| 		if (_dragging) {
 | |
| 			if (_dragging > 0) {
 | |
| 				_x = _xStart + (position - _mStart).x();
 | |
| 				_y = _yStart + (position - _mStart).y();
 | |
| 				snapXY();
 | |
| 				update();
 | |
| 			}
 | |
| 			_dragging = 0;
 | |
| 			setCursor(style::cur_default);
 | |
| 		} else if ((position - _lastAction).manhattanLength()
 | |
| 			>= st::mediaviewDeltaFromLastAction) {
 | |
| 			if (_themePreviewShown) {
 | |
| 				if (!_themePreviewRect.contains(position)) {
 | |
| 					close();
 | |
| 				}
 | |
| 			} else if (!_document
 | |
| 				|| documentContentShown()
 | |
| 				|| !documentBubbleShown()
 | |
| 				|| !_docRect.contains(position)) {
 | |
| 				close();
 | |
| 			}
 | |
| 		}
 | |
| 		_pressed = false;
 | |
| 	}
 | |
| 	_down = OverNone;
 | |
| 	if (!isHidden()) {
 | |
| 		activateControls();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::handleContextMenu(std::optional<QPoint> position) {
 | |
| 	if (position && !QRect(_x, _y, _w, _h).contains(*position)) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	_menu = base::make_unique_q<Ui::PopupMenu>(
 | |
| 		_widget,
 | |
| 		st::mediaviewPopupMenu);
 | |
| 	fillContextMenuActions([&](
 | |
| 			const QString &text,
 | |
| 			Fn<void()> handler,
 | |
| 			const style::icon *icon) {
 | |
| 		_menu->addAction(text, std::move(handler), icon);
 | |
| 	});
 | |
| 	if (_menu->empty()) {
 | |
| 		_menu = nullptr;
 | |
| 	} else {
 | |
| 		_menu->setDestroyedCallback(crl::guard(_widget, [=] {
 | |
| 			activateControls();
 | |
| 			_receiveMouse = false;
 | |
| 			InvokeQueued(_widget, [=] { receiveMouse(); });
 | |
| 		}));
 | |
| 		_menu->popup(QCursor::pos());
 | |
| 	}
 | |
| 	activateControls();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::handleTouchEvent(not_null<QTouchEvent*> e) {
 | |
| 	if (e->device()->type() != base::TouchDevice::TouchScreen) {
 | |
| 		return false;
 | |
| 	} else if (e->type() == QEvent::TouchBegin
 | |
| 		&& !e->touchPoints().isEmpty()
 | |
| 		&& _widget->childAt(
 | |
| 			_widget->mapFromGlobal(
 | |
| 				e->touchPoints().cbegin()->screenPos().toPoint()))) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	switch (e->type()) {
 | |
| 	case QEvent::TouchBegin: {
 | |
| 		if (_touchPress || e->touchPoints().isEmpty()) {
 | |
| 			break;
 | |
| 		}
 | |
| 		_touchTimer.callOnce(QApplication::startDragTime());
 | |
| 		_touchPress = true;
 | |
| 		_touchMove = _touchRightButton = false;
 | |
| 		_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchUpdate: {
 | |
| 		if (!_touchPress || e->touchPoints().isEmpty()) {
 | |
| 			break;
 | |
| 		}
 | |
| 		if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
 | |
| 			_touchMove = true;
 | |
| 		}
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchEnd: {
 | |
| 		if (!_touchPress) {
 | |
| 			break;
 | |
| 		}
 | |
| 		auto weak = Ui::MakeWeak(_widget);
 | |
| 		if (!_touchMove) {
 | |
| 			const auto button = _touchRightButton
 | |
| 				? Qt::RightButton
 | |
| 				: Qt::LeftButton;
 | |
| 			const auto position = _widget->mapFromGlobal(_touchStart);
 | |
| 
 | |
| 			if (weak) handleMousePress(position, button);
 | |
| 			if (weak) handleMouseRelease(position, button);
 | |
| 			if (weak && _touchRightButton) {
 | |
| 				handleContextMenu(position);
 | |
| 			}
 | |
| 		} else if (_touchMove) {
 | |
| 			if ((!_leftNavVisible || !_leftNav.contains(_widget->mapFromGlobal(_touchStart))) && (!_rightNavVisible || !_rightNav.contains(_widget->mapFromGlobal(_touchStart)))) {
 | |
| 				QPoint d = (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart);
 | |
| 				if (d.x() * d.x() > d.y() * d.y() && (d.x() > st::mediaviewSwipeDistance || d.x() < -st::mediaviewSwipeDistance)) {
 | |
| 					moveToNext(d.x() > 0 ? -1 : 1);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		if (weak) {
 | |
| 			_touchTimer.cancel();
 | |
| 			_touchPress = _touchMove = _touchRightButton = false;
 | |
| 		}
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchCancel: {
 | |
| 		_touchPress = false;
 | |
| 		_touchTimer.cancel();
 | |
| 	} break;
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::toggleApplicationEventFilter(bool install) {
 | |
| 	if (!install) {
 | |
| 		_applicationEventFilter = nullptr;
 | |
| 		return;
 | |
| 	} else if (_applicationEventFilter) {
 | |
| 		return;
 | |
| 	}
 | |
| 	class Filter final : public QObject {
 | |
| 	public:
 | |
| 		explicit Filter(not_null<OverlayWidget*> owner) : _owner(owner) {
 | |
| 		}
 | |
| 
 | |
| 	private:
 | |
| 		bool eventFilter(QObject *obj, QEvent *e) override {
 | |
| 			return obj && e && _owner->filterApplicationEvent(obj, e);
 | |
| 		}
 | |
| 
 | |
| 		const not_null<OverlayWidget*> _owner;
 | |
| 
 | |
| 	};
 | |
| 
 | |
| 	_applicationEventFilter = std::make_unique<Filter>(this);
 | |
| 	qApp->installEventFilter(_applicationEventFilter.get());
 | |
| }
 | |
| 
 | |
| bool OverlayWidget::filterApplicationEvent(
 | |
| 		not_null<QObject*> object,
 | |
| 		not_null<QEvent*> e) {
 | |
| 	const auto type = e->type();
 | |
| 	if (type == QEvent::ShortcutOverride) {
 | |
| 		const auto keyEvent = static_cast<QKeyEvent*>(e.get());
 | |
| 		const auto ctrl = keyEvent->modifiers().testFlag(Qt::ControlModifier);
 | |
| 		if (keyEvent->key() == Qt::Key_F && ctrl && _streamed) {
 | |
| 			playbackToggleFullScreen();
 | |
| 		}
 | |
| 		return true;
 | |
| 	} else if (type == QEvent::MouseMove
 | |
| 		|| type == QEvent::MouseButtonPress
 | |
| 		|| type == QEvent::MouseButtonRelease) {
 | |
| 		if (object->isWidgetType()
 | |
| 			&& _widget->isAncestorOf(static_cast<QWidget*>(object.get()))) {
 | |
| 			const auto mouseEvent = static_cast<QMouseEvent*>(e.get());
 | |
| 			const auto mousePosition = _widget->mapFromGlobal(
 | |
| 				mouseEvent->globalPos());
 | |
| 			const auto delta = (mousePosition - _lastMouseMovePos);
 | |
| 			auto activate = delta.manhattanLength()
 | |
| 				>= st::mediaviewDeltaFromLastAction;
 | |
| 			if (activate) {
 | |
| 				_lastMouseMovePos = mousePosition;
 | |
| 			}
 | |
| 			if (type == QEvent::MouseButtonPress) {
 | |
| 				_mousePressed = true;
 | |
| 				activate = true;
 | |
| 			} else if (type == QEvent::MouseButtonRelease) {
 | |
| 				_mousePressed = false;
 | |
| 				activate = true;
 | |
| 			}
 | |
| 			if (activate) {
 | |
| 				activateControls();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::applyHideWindowWorkaround() {
 | |
| 	// QOpenGLWidget can't properly destroy a child widget if it is hidden
 | |
| 	// exactly after that, the child is cached in the backing store.
 | |
| 	// So on next paint we force full backing store repaint.
 | |
| 	if (_opengl && !isHidden() && !_hideWorkaround) {
 | |
| 		_hideWorkaround = std::make_unique<Ui::RpWidget>(_widget);
 | |
| 		const auto raw = _hideWorkaround.get();
 | |
| 		raw->setGeometry(_widget->rect());
 | |
| 		raw->show();
 | |
| 		raw->paintRequest(
 | |
| 		) | rpl::start_with_next([=] {
 | |
| 			if (_hideWorkaround.get() == raw) {
 | |
| 				_hideWorkaround.release();
 | |
| 			}
 | |
| 			QPainter(raw).fillRect(raw->rect(), QColor(0, 1, 0, 1));
 | |
| 			crl::on_main(raw, [=] {
 | |
| 				delete raw;
 | |
| 			});
 | |
| 		}, raw->lifetime());
 | |
| 		raw->update();
 | |
| 
 | |
| 		if (Platform::IsWindows()) {
 | |
| 			Ui::Platform::UpdateOverlayed(_widget);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| Window::SessionController *OverlayWidget::findWindow(bool switchTo) const {
 | |
| 	if (!_session) {
 | |
| 		return nullptr;
 | |
| 	}
 | |
| 
 | |
| 	const auto window = _window.get();
 | |
| 	if (window) {
 | |
| 		if (const auto controller = window->sessionController()) {
 | |
| 			if (&controller->session() == _session) {
 | |
| 				return controller;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (switchTo) {
 | |
| 		auto controllerPtr = (Window::SessionController*)nullptr;
 | |
| 		const auto anyWindow = window ? window : Core::App().primaryWindow();
 | |
| 		if (anyWindow) {
 | |
| 			anyWindow->invokeForSessionController(
 | |
| 				&_session->account(),
 | |
| 				_history ? _history->peer.get() : nullptr,
 | |
| 				[&](not_null<Window::SessionController*> newController) {
 | |
| 					controllerPtr = newController;
 | |
| 				});
 | |
| 		}
 | |
| 		return controllerPtr;
 | |
| 	}
 | |
| 
 | |
| 	return nullptr;
 | |
| }
 | |
| 
 | |
| // #TODO unite and check
 | |
| void OverlayWidget::clearBeforeHide() {
 | |
| 	_sharedMedia = nullptr;
 | |
| 	_sharedMediaData = std::nullopt;
 | |
| 	_sharedMediaDataKey = std::nullopt;
 | |
| 	_userPhotos = nullptr;
 | |
| 	_userPhotosData = std::nullopt;
 | |
| 	_collage = nullptr;
 | |
| 	_collageData = std::nullopt;
 | |
| 	clearStreaming();
 | |
| 	assignMediaPointer(nullptr);
 | |
| 	_preloadPhotos.clear();
 | |
| 	_preloadDocuments.clear();
 | |
| 	if (_menu) {
 | |
| 		_menu->hideMenu(true);
 | |
| 	}
 | |
| 	_controlsHideTimer.cancel();
 | |
| 	_controlsState = ControlsShown;
 | |
| 	_controlsOpacity = anim::value(1, 1);
 | |
| 	_groupThumbs = nullptr;
 | |
| 	_groupThumbsRect = QRect();
 | |
| 	for (const auto child : _widget->children()) {
 | |
| 		if (child->isWidgetType() && _hideWorkaround.get() != child) {
 | |
| 			static_cast<QWidget*>(child)->hide();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::clearAfterHide() {
 | |
| 	clearStreaming();
 | |
| 	destroyThemePreview();
 | |
| 	_radial.stop();
 | |
| 	_staticContent = QImage();
 | |
| 	_themePreview = nullptr;
 | |
| 	_themeApply.destroyDelayed();
 | |
| 	_themeCancel.destroyDelayed();
 | |
| 	_themeShare.destroyDelayed();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::receiveMouse() {
 | |
| 	_receiveMouse = true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::showDropdown() {
 | |
| 	_dropdown->clearActions();
 | |
| 	fillContextMenuActions([&](
 | |
| 			const QString &text,
 | |
| 			Fn<void()> handler,
 | |
| 			const style::icon *icon) {
 | |
| 		_dropdown->addAction(text, std::move(handler), icon);
 | |
| 	});
 | |
| 	_dropdown->moveToRight(0, height() - _dropdown->height());
 | |
| 	_dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
 | |
| 	_dropdown->setFocus();
 | |
| }
 | |
| 
 | |
| void OverlayWidget::handleTouchTimer() {
 | |
| 	_touchRightButton = true;
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateImage() {
 | |
| 	update(_saveMsg);
 | |
| }
 | |
| 
 | |
| void OverlayWidget::findCurrent() {
 | |
| 	using namespace rpl::mappers;
 | |
| 	if (_sharedMediaData) {
 | |
| 		_index = _message
 | |
| 			? _sharedMediaData->indexOf(_message->fullId())
 | |
| 			: _photo ? _sharedMediaData->indexOf(_photo) : std::nullopt;
 | |
| 		_fullIndex = _sharedMediaData->skippedBefore()
 | |
| 			? (_index | func::add(*_sharedMediaData->skippedBefore()))
 | |
| 			: std::nullopt;
 | |
| 		_fullCount = _sharedMediaData->fullCount();
 | |
| 	} else if (_userPhotosData) {
 | |
| 		_index = _photo ? _userPhotosData->indexOf(_photo->id) : std::nullopt;
 | |
| 		_fullIndex = _userPhotosData->skippedBefore()
 | |
| 			? (_index | func::add(*_userPhotosData->skippedBefore()))
 | |
| 			: std::nullopt;
 | |
| 		_fullCount = _userPhotosData->fullCount();
 | |
| 	} else if (_collageData) {
 | |
| 		const auto item = _photo ? WebPageCollage::Item(_photo) : _document;
 | |
| 		const auto &items = _collageData->items;
 | |
| 		const auto i = ranges::find(items, item);
 | |
| 		_index = (i != end(items))
 | |
| 			? std::make_optional(int(i - begin(items)))
 | |
| 			: std::nullopt;
 | |
| 		_fullIndex = _index;
 | |
| 		_fullCount = items.size();
 | |
| 	} else {
 | |
| 		_index = _fullIndex = _fullCount = std::nullopt;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void OverlayWidget::updateHeader() {
 | |
| 	auto index = _fullIndex ? *_fullIndex : -1;
 | |
| 	auto count = _fullCount ? *_fullCount : -1;
 | |
| 	if (index >= 0 && index < count && count > 1) {
 | |
| 		if (_document) {
 | |
| 			_headerText = tr::lng_mediaview_file_n_of_amount(
 | |
| 				tr::now,
 | |
| 				lt_file,
 | |
| 				(_document->filename().isEmpty()
 | |
| 					? tr::lng_mediaview_doc_image(tr::now)
 | |
| 					: _document->filename()),
 | |
| 				lt_n,
 | |
| 				QString::number(index + 1),
 | |
| 				lt_amount,
 | |
| 				QString::number(count));
 | |
| 		} else {
 | |
| 			if (_user
 | |
| 				&& (index == count - 1)
 | |
| 				&& SyncUserFallbackPhotoViewer(_user)) {
 | |
| 				_headerText = tr::lng_mediaview_profile_public_photo(tr::now);
 | |
| 			} else {
 | |
| 				_headerText = tr::lng_mediaview_n_of_amount(
 | |
| 					tr::now,
 | |
| 					lt_n,
 | |
| 					QString::number(index + 1),
 | |
| 					lt_amount,
 | |
| 					QString::number(count));
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (_document) {
 | |
| 			_headerText = _document->filename().isEmpty()
 | |
| 				? tr::lng_mediaview_doc_image(tr::now)
 | |
| 				: _document->filename();
 | |
| 		} else if (_message) {
 | |
| 			_headerText = tr::lng_mediaview_single_photo(tr::now);
 | |
| 		} else if (_user) {
 | |
| 			_headerText = tr::lng_mediaview_profile_photo(tr::now);
 | |
| 		} else if ((_history && _history->peer->isBroadcast())
 | |
| 			|| (_peer && _peer->isChannel() && !_peer->isMegagroup())) {
 | |
| 			_headerText = tr::lng_mediaview_channel_photo(tr::now);
 | |
| 		} else if (_peer) {
 | |
| 			_headerText = tr::lng_mediaview_group_photo(tr::now);
 | |
| 		} else {
 | |
| 			_headerText = tr::lng_mediaview_single_photo(tr::now);
 | |
| 		}
 | |
| 	}
 | |
| 	_headerHasLink = computeOverviewType() != std::nullopt;
 | |
| 	auto hwidth = st::mediaviewThickFont->width(_headerText);
 | |
| 	if (hwidth > width() / 3) {
 | |
| 		hwidth = width() / 3;
 | |
| 		_headerText = st::mediaviewThickFont->elided(_headerText, hwidth, Qt::ElideMiddle);
 | |
| 	}
 | |
| 	_headerNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height);
 | |
| }
 | |
| 
 | |
| float64 OverlayWidget::overLevel(OverState control) const {
 | |
| 	auto i = _animationOpacities.find(control);
 | |
| 	return (i == end(_animationOpacities))
 | |
| 		? (_over == control ? 1. : 0.)
 | |
| 		: i->second.current();
 | |
| }
 | |
| 
 | |
| } // namespace View
 | |
| } // namespace Media
 | 
