/* 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 #include #include #include #include #include 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 session); void pipSaveGeometry(QByteArray geometry) override; QByteArray pipLoadGeometry() override; float64 pipPlaybackSpeed() override; QWidget *pipParentWidget() override; private: QWidget *_parent = nullptr; not_null _session; }; PipDelegate::PipDelegate(QWidget *parent, not_null 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(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 document, Data::FileOrigin origin, not_null controlsParent, not_null controlsDelegate, Fn waitingCallback); Streamed( not_null photo, Data::FileOrigin origin, not_null controlsParent, not_null controlsDelegate, Fn waitingCallback); Streaming::Instance instance; PlaybackControls controls; std::unique_ptr powerSaveBlocker; bool withSound = false; bool pausedBySeek = false; bool resumeOnCallEnd = false; }; struct OverlayWidget::PipWrap { PipWrap( QWidget *parent, not_null document, std::shared_ptr shared, FnMut closeAndContinue, FnMut destroy); PipWrap(const PipWrap &other) = delete; PipWrap &operator=(const PipWrap &other) = delete; PipDelegate delegate; Pip wrapped; }; OverlayWidget::Streamed::Streamed( not_null document, Data::FileOrigin origin, not_null controlsParent, not_null controlsDelegate, Fn waitingCallback) : instance(document, origin, std::move(waitingCallback)) , controls(controlsParent, controlsDelegate) { } OverlayWidget::Streamed::Streamed( not_null photo, Data::FileOrigin origin, not_null controlsParent, not_null controlsDelegate, Fn waitingCallback) : instance(photo, origin, std::move(waitingCallback)) , controls(controlsParent, controlsDelegate) { } OverlayWidget::PipWrap::PipWrap( QWidget *parent, not_null document, std::shared_ptr shared, FnMut closeAndContinue, FnMut 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 e) { return static_cast(e.get())->pos(); }; const auto mouseButton = [](not_null e) { return static_cast(e.get())->button(); }; base::install_event_filter(_widget, [=](not_null e) { const auto type = e->type(); if (type == QEvent::Move) { const auto position = static_cast(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(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(e.get())); } else if (type == QEvent::ContextMenu) { const auto event = static_cast(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(e.get()))) { return base::EventFilterResult::Cancel;; } } else if (type == QEvent::Wheel) { handleWheelEvent(static_cast(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(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(_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 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, cDateFormat(), cTimeFormat()); 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(); 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 { 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 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(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( topic, Info::Section(*overviewType)) : std::make_shared( _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 { 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 { 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>(entity.data)) { return Data::FileOrigin(); } const auto photo = v::get>(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(); }; }; 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(*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() 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(*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() 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(*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(&key)) { const auto photo = _session->data().photo(*photoId); moveToEntity({ photo, nullptr }); } else if (const auto itemId = std::get_if(&key)) { moveToEntity(entityForItemId(*itemId)); } else if (const auto collageKey = std::get_if(&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 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 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 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(); 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( _document, fileOrigin(), _widget, static_cast(this), [=] { waitingAnimationCallback(); }); } else { _streamed = std::make_unique( _photo, fileOrigin(), _widget, static_cast(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(); 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 message = _message; const auto topicRootId = _topicRootId; const auto closeAndContinue = [=] { _showAsPip = false; show(OpenRequest( findWindow(false), document, message, topicRootId, true)); }; _showAsPip = true; _pip = std::make_unique( _widget, document, _streamed->instance.shared(), closeAndContinue, [=] { _pip = nullptr; }); 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(this), .backend = Ui::GL::Backend::OpenGL, }; } return { .renderer = std::make_unique(this), .backend = Ui::GL::Backend::Raster, }; } void OverlayWidget::paint(not_null 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) { 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, 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 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 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>(&value)) { // Last peer photo. return { *photo, nullptr }; } else if (const auto itemId = std::get_if(&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(&items[index])) { return { *document, _message, _topicRootId }; } else if (const auto photo = std::get_if(&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> context) { if (const auto item = std::get_if(&context)) { _message = item->item; _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); } else if (const auto peer = std::get_if>(&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 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 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 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>(&entity.data)) { displayPhoto(*photo); } else if (auto document = std::get_if>(&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>(); auto documents = base::flat_set>(); for (auto index = from; index != till + 1; ++index) { auto entity = entityByIndex(index); if (auto photo = std::get_if>(&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>( &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 position) { if (position && !QRect(_x, _y, _w, _h).contains(*position)) { return false; } _menu = base::make_unique_q( _widget, st::mediaviewPopupMenu); fillContextMenuActions([&]( const QString &text, Fn 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 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 owner) : _owner(owner) { } private: bool eventFilter(QObject *obj, QEvent *e) override { return obj && e && _owner->filterApplicationEvent(obj, e); } const not_null _owner; }; _applicationEventFilter = std::make_unique(this); qApp->installEventFilter(_applicationEventFilter.get()); } bool OverlayWidget::filterApplicationEvent( not_null object, not_null e) { const auto type = e->type(); if (type == QEvent::ShortcutOverride) { const auto keyEvent = static_cast(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(object.get()))) { const auto mouseEvent = static_cast(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(_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 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(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 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 { _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