diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp new file mode 100644 index 0000000..9ee7762 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -0,0 +1,9035 @@ +/* +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 "api/api_polls.h" +#include "base/base_file_utilities.h" +#include "base/qt/qt_common_adapters.h" +#include "base/timer_rpl.h" +#include "lang/lang_keys.h" +#include "menu/menu_sponsored.h" +#include "boxes/premium_preview_box.h" +#include "calls/calls_instance.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "core/core_screenshot_protection.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "core/ui_integration.h" +#include "core/crash_reports.h" +#include "core/sandbox.h" +#include "core/shortcuts.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/controls/swipe_handler.h" +#include "ui/controls/swipe_handler_data.h" +#include "ui/controls/ttl_media.h" +#include "ui/layers/layer_manager.h" +#include "ui/text/text_utilities.h" +#include "ui/chat/chat_style.h" +#include "ui/platform/ui_platform_window_title.h" +#include "ui/toast/toast.h" +#include "ui/text/format_values.h" +#include "ui/item_text_options.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/power_saving.h" +#include "ui/cached_round_corners.h" +#include "ui/gl/gl_window.h" +#include "ui/boxes/confirm_box.h" +#include "ui/ui_utility.h" +#include "info/info_memento.h" +#include "info/info_controller.h" +#include "info/statistics/info_statistics_widget.h" +#include "boxes/delete_messages_box.h" +#include "boxes/moderate_messages_box.h" +#include "boxes/report_messages_box.h" +#include "media/audio/media_audio.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/view/media_view_overlay_rhi.h" +#include "media/view/media_view_playback_sponsored.h" +#include "media/view/media_view_video_stream.h" +#include "media/stories/media_stories_share.h" +#include "media/stories/media_stories_view.h" +#include "media/streaming/media_streaming_document.h" +#include "media/streaming/media_streaming_player.h" +#include "media/player/media_player_instance.h" +#include "history/history.h" +#include "history/history_item_helpers.h" +#include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_media_common.h" +#include "history/view/history_view_group_call_bar.h" +#include "history/view/reactions/history_view_reactions_selector.h" +#include "data/components/sponsored_messages.h" +#include "data/data_group_call.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_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 "data/data_poll.h" +#include "window/themes/window_theme_preview.h" +#include "window/window_peer_menu.h" +#include "window/window_controller.h" +#include "base/platform/base_platform_haptic.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/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 "platform/platform_overlay_widget.h" +#include "platform/platform_specific.h" +#include "storage/file_download.h" +#include "storage/storage_account.h" +#include "styles/style_chat_style.h" +#include "styles/style_media_view.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_dialogs.h" +#include "styles/style_menu_icons.h" +#include "styles/style_ttl_media.h" +#include "platform/platform_text_recognition.h" + +#include + +#ifdef Q_OS_MAC +#include "platform/mac/touchbar/mac_touchbar_media_view.h" +#endif // Q_OS_MAC + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Media { +namespace View { +namespace { + +[[nodiscard]] int LayoutIndependentKey(not_null e) { + if constexpr (Platform::IsWindows()) { + const auto native = int(e->nativeVirtualKey()); + if (native >= Qt::Key_A && native <= Qt::Key_Z) { + return native; + } + } + return e->key(); +} + +[[nodiscard]] bool InstantViewMediaItemMatches( + const HistoryMessageMediaForInstantView::Item &item, + PhotoData *photo, + DocumentData *document) { + if (const auto itemPhoto = std::get_if(&item)) { + return *itemPhoto == photo; + } else if (const auto itemDocument = std::get_if(&item)) { + return *itemDocument == document; + } + return false; +} + +[[nodiscard]] std::optional InstantViewMediaCaption( + HistoryItem *message, + PhotoData *photo, + DocumentData *document) { + if (!message || (!photo && !document)) { + return std::nullopt; + } + const auto media = message->Get(); + if (!media) { + return std::nullopt; + } + for (auto i = size_t(); i != media->items.size(); ++i) { + if (InstantViewMediaItemMatches(media->items[i], photo, document)) { + return (i < media->captions.size()) + ? media->captions[i] + : TextWithEntities(); + } + } + return std::nullopt; +} + +constexpr auto kPreloadCount = 3; +constexpr auto kMaxZoomLevel = 7; // x8 +constexpr auto kZoomToScreenLevel = 1024; +constexpr auto kPinchZoomStep = 0.25; +constexpr auto kOverlayLoaderPriority = 2; +constexpr auto kSeekTimeMs = 5 * crl::time(1000); +constexpr auto kSeekTimeMsLong = 10 * crl::time(1000); +constexpr auto kFrameStepFallbackFps = 30.; +constexpr auto kFrameStepThrottleMs = crl::time(150); + +// 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; + +constexpr auto kLeftSiblingTextureIndex = 1; +constexpr auto kRightSiblingTextureIndex = 2; +constexpr auto kStoriesControlsOpacity = 1.; +constexpr auto kStorySavePromoDuration = 3 * crl::time(1000); + +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; + +}; + +[[nodiscard]] Core::WindowPosition DefaultPosition() { + const auto moncrc = [&] { + if (const auto active = Core::App().activeWindow()) { + const auto widget = active->widget(); + if (const auto screen = widget->screen()) { + return Platform::ScreenNameChecksum(screen->name()); + } + } + return Core::App().settings().windowPosition().moncrc; + }(); + return { + .moncrc = moncrc, + .scale = cScale(), + .x = st::mediaviewDefaultLeft, + .y = st::mediaviewDefaultTop, + .w = st::mediaviewDefaultWidth, + .h = st::mediaviewDefaultHeight, + }; +} + +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; +} + +[[nodiscard]] std::optional PollInputMediaItem( + const PollMedia &media) { + if (media.photo) { + return WebPageCollage::Item(media.photo); + } else if (media.document && !media.document->sticker()) { + return WebPageCollage::Item(media.document); + } + return std::nullopt; +} + +[[nodiscard]] std::optional PollAnswerMediaItem( + const PollAnswer &answer) { + return PollInputMediaItem(answer.media); +} + +[[nodiscard]] bool IsPollAnswerMediaItem( + not_null poll, + const WebPageCollage::Item &item) { + return ranges::any_of(poll->answers, [&](const PollAnswer &answer) { + const auto candidate = PollAnswerMediaItem(answer); + return candidate && (*candidate == item); + }); +} + +[[nodiscard]] std::optional PollAnswersCollage( + not_null poll) { + auto result = WebPageCollage(); + for (const auto &answer : poll->answers) { + if (const auto item = PollAnswerMediaItem(answer)) { + result.items.push_back(*item); + } + } + return result.items.empty() + ? std::nullopt + : std::make_optional(std::move(result)); +} + +void RefreshCaptionQuoteCaches( + Ui::Text::QuotePaintCache &blockquote, + Ui::Text::QuotePaintCache &pre) { + const auto withAlpha = [](QColor color, float64 opacity) { + color.setAlpha(int(opacity * 255)); + return color; + }; + + const auto accent = st::mediaviewTextLinkFg->c; + blockquote.bg = withAlpha(accent, Ui::kDefaultBgOpacity); + blockquote.outlines[0] = withAlpha(accent, Ui::kDefaultOutline1Opacity); + blockquote.outlines[1] = blockquote.outlines[2] = QColor(0, 0, 0, 0); + blockquote.icon = accent; + + const auto mono = st::mediaviewCaptionFg->c; + pre.bg = QColor(0, 0, 0, 192); + pre.outlines[0] = withAlpha(mono, Ui::kDefaultOutline1Opacity); + pre.outlines[1] = pre.outlines[2] = QColor(0, 0, 0, 0); + pre.header = withAlpha(mono, Ui::kDefaultOutline2Opacity); + pre.icon = withAlpha(mono, Ui::kDefaultOutline3Opacity); +} + +} // namespace + +class OverlayWidget::SponsoredButton : public Ui::RippleButton { +public: + SponsoredButton(QWidget *parent) + : Ui::RippleButton(parent, st::mediaviewSponsoredButton.ripple) { + } + + void setText(QString text) { + _text = Ui::Text::String( + st::mediaviewSponsoredButton.style, + std::move(text), + kDefaultTextOptions, + width()); + resize(width(), _text.minHeight() * 2); + } + void setOpacity(float opacity) { + _opacity = opacity; + } + +protected: + void paintEvent(QPaintEvent *e) override { + auto p = QPainter(this); + const auto &st = st::mediaviewSponsoredButton; + + p.setOpacity(_opacity); + + const auto over = Ui::AbstractButton::isOver(); + const auto down = Ui::AbstractButton::isDown(); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush((over || down) ? st.textBgOver : st.textBg); + p.drawRoundedRect( + rect(), + st::mediaviewCaptionRadius, + st::mediaviewCaptionRadius); + } + + Ui::RippleButton::paintRipple(p, 0, 0); + + p.setPen(st.textFg); + p.setBrush(Qt::NoBrush); + _text.draw(p, { + .position = QPoint( + (width() - _text.maxWidth()) / 2, + (height() - _text.minHeight()) / 2), + .outerWidth = width(), + .availableWidth = width(), + }); + } + + QImage prepareRippleMask() const override { + return Ui::RippleAnimation::RoundRectMask( + size(), + st::mediaviewCaptionRadius); + } + QPoint prepareRippleStartPosition() const override { + return mapFromGlobal(QCursor::pos()) + - rect::m::pos::tl(st::mediaviewSponsoredButton.padding); + } + +private: + Ui::Text::String _text; + float64 _opacity = 1.; + +}; + + +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::InstantViewMedia { + InstantViewMedia(HistoryItem *item, InstantViewItem key) + : item(item) + , key(std::move(key)) { + } + + HistoryItem *item = nullptr; + InstantViewItem key; +}; + +struct OverlayWidget::Collage { + Collage(CollageKey key) : key(key) { + } + + CollageKey key; +}; + +struct OverlayWidget::Streamed { + Streamed( + not_null quality, + not_null original, + HistoryItem *context, + Data::FileOrigin origin, + Fn waitingCallback); + Streamed( + not_null photo, + Data::FileOrigin origin, + Fn waitingCallback); + + Streaming::Instance instance; + std::unique_ptr controls; + std::unique_ptr sponsored; + std::unique_ptr powerSaveBlocker; + + bool ready = false; + bool withSound = false; + bool pausedBySeek = false; + bool resumeOnCallEnd = false; +}; + +struct OverlayWidget::PipWrap { + PipWrap( + QWidget *parent, + not_null document, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, + std::shared_ptr shared, + FnMut closeAndContinue, + FnMut destroy); + + PipWrap(const PipWrap &other) = delete; + PipWrap &operator=(const PipWrap &other) = delete; + + PipDelegate delegate; + Pip wrapped; + rpl::lifetime lifetime; +}; + +struct OverlayWidget::ItemContext { + not_null item; + MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; +}; + +struct OverlayWidget::StoriesContext { + not_null peer; + StoryId id = 0; + Data::StoriesContext within; +}; + +class OverlayWidget::Show final : public ChatHelpers::Show { +public: + explicit Show(not_null widget) : _widget(widget) { + } + + void activate() override { + if (!_widget->isHidden()) { + _widget->activate(); + } + } + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override { + _widget->_layerBg->uiShow()->showOrHideBoxOrLayer( + std::move(layer), + options, + anim::type::normal); + } + not_null toastParent() const override { + return _widget->_body; + } + bool valid() const override { + return _widget->_session || _widget->_storiesSession; + } + operator bool() const override { + return valid(); + } + + Main::Session &session() const override { + Expects(_widget->_session || _widget->_storiesSession); + + return _widget->_session + ? *_widget->_session + : *_widget->_storiesSession; + } + bool paused(ChatHelpers::PauseReason reason) const override { + if (_widget->isHidden() + || (!_widget->_fullscreen + && !_widget->_window->isActiveWindow())) { + return true; + } else if (reason < ChatHelpers::PauseReason::Layer + && _widget->_layerBg->topShownLayer() != nullptr) { + return true; + } + return false; + } + rpl::producer<> pauseChanged() const override { + return rpl::never<>(); + } + + rpl::producer adjustShadowLeft() const override { + return rpl::single(false); + } + SendMenu::Details sendMenuDetails() const override { + return { SendMenu::Type::SilentOnly }; + } + + Window::SessionController *resolveWindow() const override { + return _widget->findWindow(); + } + + bool showMediaPreview( + Data::FileOrigin origin, + not_null document) const override { + return false; // #TODO stories + } + bool showMediaPreview( + Data::FileOrigin origin, + not_null photo) const override { + return false; // #TODO stories + } + + void processChosenSticker( + ChatHelpers::FileChosen &&chosen) const override { + _widget->_storiesStickerOrEmojiChosen.fire(std::move(chosen)); + } + +private: + not_null _widget; + +}; + +OverlayWidget::Streamed::Streamed( + not_null quality, + not_null original, + HistoryItem *context, + Data::FileOrigin origin, + Fn waitingCallback) +: instance(quality, original, context, origin, std::move(waitingCallback)) { +} + +OverlayWidget::Streamed::Streamed( + not_null photo, + Data::FileOrigin origin, + Fn waitingCallback) +: instance(photo, origin, std::move(waitingCallback)) { +} + +OverlayWidget::PipWrap::PipWrap( + QWidget *parent, + not_null document, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, + std::shared_ptr shared, + FnMut closeAndContinue, + FnMut destroy) +: delegate(parent, &document->session()) +, wrapped( + &delegate, + document, + origin, + chosenQuality, + context, + quality, + std::move(shared), + std::move(closeAndContinue), + std::move(destroy)) { +} + +OverlayWidget::OverlayWidget() +: _wrap(std::make_unique(Ui::GL::Window::Translucent::Yes)) +, _window(_wrap->window()) +, _helper(Platform::CreateOverlayWidgetHelper(_window.get(), [=](bool maximized) { + toggleFullScreen(maximized); +})) +, _body(_wrap->widget()) +, _titleBugWorkaround(std::make_unique(_body)) +, _surface( + Ui::GL::CreateSurface(_body, chooseRenderer(_wrap->backend()))) +, _widget(_surface->rpWidget()) +, _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2) +, _windowed(Core::App().settings().mediaViewPosition().maximized == 0) +, _quality(Core::App().settings().videoQuality()) +, _layerBg(std::make_unique(_body)) +, _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink) +, _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) +, _docCancel(_body, 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(_body, st::mediaviewDropdownMenu) +, _speedBoostTicker([=] { + updateSpeedBoost(); + return isSpeedBoostShown(); +}) { + _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); + _layerBg->setHideByBackgroundClick(true); + + _recognition.setSources(&_recognitionResult, &_staticContent); + + CrashReports::SetAnnotation("OpenGL Renderer", "[not-initialized]"); + + Lang::Updated( + ) | rpl::on_next([=] { + refreshLang(); + }, lifetime()); + + _lastPositiveVolume = (Core::App().settings().videoVolume() > 0.) + ? Core::App().settings().videoVolume() + : Core::Settings::kDefaultVolume; + + _saveMsgTimer.setCallback([=, delay = st::mediaviewSaveMsgHiding] { + _saveMsgAnimation.start([=] { updateSaveMsg(); }, 1., 0., delay); + }); + + _ttlTimer.setCallback([=] { + updateControls(); + _widget->update(); + }); + + _chapterTimer.setCallback([=, delay = st::mediaviewChapterHiding] { + _chapterAnimation.start([=] { updateChapter(); }, 1., 0., delay); + }); + + _speedBoostHoldTimer.setCallback([=] { + startSpeedBoost(); + }); + + _frameStepThrottle.setCallback([=] { + flushPendingFrameStep(); + }); + + _docRectImage = QImage( + st::mediaviewFileSize * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + _docRectImage.setDevicePixelRatio(style::DevicePixelRatio()); + + Shortcuts::Requests( + ) | rpl::on_next([=](not_null request) { + request->check( + Shortcuts::Command::MediaViewerFullscreen + ) && request->handle([=] { + if (_streamed) { + playbackToggleFullScreen(); + return true; + } + return false; + }); + }, lifetime()); + + setupWindow(); + + 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(_window, [=](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())); + if (_windowed) { + savePosition(); + } else { + 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())); + if (_windowed) { + savePosition(); + } + } else if (type == QEvent::Close + && !Core::Sandbox::Instance().isSavingSession() + && !Core::Quitting()) { + e->ignore(); + close(); + return base::EventFilterResult::Cancel; + } else if (type == QEvent::ThemeChange && Platform::IsLinux()) { + _window->setWindowIcon(Window::CreateIcon(_session)); + } 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 (e->type() == QEvent::WindowStateChange) { + const auto state = window()->windowState(); + if (state == Qt::WindowMinimized || Platform::IsMac()) { + } else if (state == Qt::WindowMaximized) { + if (_fullscreen || _windowed) { + _fullscreen = _windowed = false; + savePosition(); + } + } else if (_fullscreen || _windowed) { + } else if (state == Qt::WindowFullScreen) { + _fullscreen = true; + savePosition(); + } else { + _windowed = true; + savePosition(); + } + } + return base::EventFilterResult::Continue; + }); + base::install_event_filter(_body, [=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Resize) { + const auto size = static_cast(e.get())->size(); + + // Somehow Windows 11 knows the geometry of first widget below + // the semi-native title control widgets and it uses + // it's geometry to show the snap grid popup around it when + // you put the mouse over the Maximize button. In the 4.6.4 beta + // the first widget was `_widget`, so the popup was shown + // either above the window or, if not enough space above, below + // the whole window, you couldn't even put the mouse on it. + // + // So now here is this weird workaround that places our + // `_titleBugWorkaround` widget as the first one under the title + // controls and the system shows the popup around its geometry, + // so we set it's height to the title controls height + // and everything works as expected. + // + // This doesn't make sense. But it works. :shrug: + _titleBugWorkaround->setGeometry( + { 0, 0, size.width(), st::mediaviewTitleButton.height }); + + _widget->setGeometry({ QPoint(), size }); + updateControlsGeometry(); + } else if (type == QEvent::KeyPress) { + handleKeyPress(static_cast(e.get())); + } else if (type == QEvent::KeyRelease) { + handleKeyRelease(static_cast(e.get())); + } + return base::EventFilterResult::Continue; + }); + base::install_event_filter(_widget, [=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Leave) { + if (_over != Over::None) { + updateOverState(Over::None); + } + } 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::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())); + } else if (type == QEvent::NativeGesture) { + if (handleNativeGesture(static_cast(e.get()))) { + return base::EventFilterResult::Cancel; + } + } + return base::EventFilterResult::Continue; + }); + setupSwipeNavigation(); + _helper->mouseEvents( + ) | rpl::on_next([=](not_null e) { + if (_helper->skipTitleHitTest(e->windowPos().toPoint())) { + return; + } + const auto type = e->type(); + const auto position = e->pos(); + if (type == QEvent::MouseButtonPress) { + handleMousePress(position, e->button()); + } else if (type == QEvent::MouseButtonRelease) { + handleMouseRelease(position, e->button()); + } else if (type == QEvent::MouseMove) { + handleMouseMove(position); + } else if (type == QEvent::MouseButtonDblClick) { + if (!handleDoubleClick(position, e->button())) { + handleMousePress(position, e->button()); + } + } + }, lifetime()); + _topShadowRight = _helper->controlsSideRightValue(); + _topShadowRight.changes( + ) | rpl::on_next([=] { + updateControlsGeometry(); + update(); + }, lifetime()); + + _helper->topNotchSkipValue( + ) | rpl::on_next([=](int notch) { + if (_topNotchSize != notch) { + _topNotchSize = notch; + if (_fullscreen) { + updateControlsGeometry(); + } + } + }, lifetime()); + + _window->setTitle(tr::lng_mediaview_title(tr::now)); + _window->setTitleStyle(st::mediaviewTitle); + + 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. + _window->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + } + _widget->setMouseTracking(true); + + // Toggling between windowed and fullscreen changes the window flags, + // and that is a path where Qt recreates the native window, dropping + // everything set on the old one. Reapply on every handle change. + _window->winIdValue( + ) | rpl::on_next([=](WId) { + Platform::SetWindowScreenshotProtection( + _window, + _screenshotProtected); + }, lifetime()); + + _window->screenValue( + ) | rpl::skip(1) | rpl::on_next([=](not_null screen) { + handleScreenChanged(screen); + }, lifetime()); + subscribeToScreenGeometry(); + updateGeometry(); + updateControlsGeometry(); + +#ifdef Q_OS_MAC + TouchBar::SetupMediaViewTouchBar( + _window->winId(), + tr::lng_mediaview_title(tr::now), + 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::on_next([=](bool call) { + if (!_streamed + || !_document + || (_document->isAnimation() && !_document->isVideoMessage())) { + return; + } else if (call) { + playbackPauseOnCall(); + } else { + playbackResumeOnCall(); + } + }, lifetime()); + + _widget->setAttribute(Qt::WA_AcceptTouchEvents); + _touchTimer.setCallback([=] { handleTouchTimer(); }); + + _controlsHideTimer.setCallback([=] { hideControls(); }); + _helper->controlsActivations( + ) | rpl::on_next([=] { + activateControls(); + }, lifetime()); + + _docDownload->addClickHandler([=] { downloadMedia(); }); + _docSaveAs->addClickHandler([=] { saveAs(); }); + _docCancel->addClickHandler([=] { saveCancel(); }); + + _dropdown->setHiddenCallback([this] { dropdownHidden(); }); + _dropdownShowTimer.setCallback([=] { showDropdown(); }); + + orderWidgets(); +} + +void OverlayWidget::showSaveMsgToast(const QString &path, auto phrase) { + showSaveMsgToastWith( + path, + phrase( + tr::now, + lt_downloads, + tr::link( + tr::lng_mediaview_downloads(tr::now), + "internal:show_saved_message"), + tr::marked), + st::mediaviewSaveMsgShown); +} + +void OverlayWidget::showSaveMsgToastWith( + const QString &path, + const TextWithEntities &text, + crl::time duration) { + _saveMsgFilename = path; + _saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text); + const auto w = _saveMsgText.maxWidth() + + st::mediaviewSaveMsgPadding.left() + + st::mediaviewSaveMsgPadding.right(); + const auto h = st::mediaviewSaveMsgStyle.font->height + + st::mediaviewSaveMsgPadding.top() + + st::mediaviewSaveMsgPadding.bottom(); + _saveMsg = QRect( + (width() - w) / 2, + _minUsedTop + (_maxUsedHeight - h) / 2, + w, + h); + const auto callback = [=](float64 value) { + updateSaveMsg(); + if (!_saveMsgAnimation.animating()) { + _saveMsgTimer.callOnce(duration); + } + }; + const auto animDuration = st::mediaviewSaveMsgShowing; + _saveMsgAnimation.start(callback, 0., 1., animDuration); + updateSaveMsg(); +} + +void OverlayWidget::orderWidgets() { + _helper->orderWidgets(); +} + +void OverlayWidget::setupWindow() { + _window->setBodyTitleArea([=](QPoint widgetPoint) { + using Flag = Ui::WindowTitleHitTestFlag; + Ui::WindowTitleHitTestFlags result; + if (!_widget->rect().contains(widgetPoint) + || _helper->skipTitleHitTest(_widget->mapTo(_window, widgetPoint))) { + return result; + } + if (widgetPoint.y() <= st::mediaviewTitleButton.height) { + result |= Flag::Menu | Flag::FullScreen; + } + const auto inControls = ((_over != Over::None) && (_over != Over::Video)); + if (inControls + || (_streamed + && _streamed->controls + && _streamed->controls->dragging())) { + } else if ((_w > _widget->width() || _h > _maxUsedHeight) + && (widgetPoint.y() > st::mediaviewHeaderTop) + && QRect(_x, _y, _w, _h).contains(widgetPoint)) { + } else if (_stories && _stories->ignoreWindowMove(widgetPoint)) { + } else if (_sponsoredButton + && _sponsoredButton->geometry().contains(widgetPoint)) { + } else if (_showRecognitionResults + && _recognitionResult.success + && !_recognitionResult.items.empty() + && _recognition.positionAt( + widgetPoint, + finalContentRect(), + _rotation, + false).item >= 0) { + } else if (_windowed) { + result |= Flag::Move; + } + return result; + }); + + _window->setAttribute(Qt::WA_NoSystemBackground, true); + _window->setAttribute(Qt::WA_TranslucentBackground, true); + + _window->setMinimumSize( + { st::mediaviewMinWidth, st::mediaviewMinHeight }); + + _window->shownValue( + ) | rpl::on_next([=](bool shown) { + toggleApplicationEventFilter(shown); + if (!shown) { + clearAfterHide(); + } else { + const auto geometry = _window->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(_window->screen()))); + moveToScreen(); + } + }, lifetime()); +} + +void OverlayWidget::refreshLang() { + InvokeQueued(_widget, [=] { updateThemePreviewGeometry(); }); +} + +void OverlayWidget::moveToScreen(bool inMove) { + if (!_fullscreen || _wasWindowedMode) { + return; + } + const auto applicationWindow = Core::App().activeWindow() + ? Core::App().activeWindow()->widget().get() + : nullptr; + const auto activeWindowScreen = applicationWindow + ? applicationWindow->screen() + : nullptr; + const auto myScreen = _window->screen(); + 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))); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + _window->setScreen(activeWindowScreen); +#else // Qt >= 6.0.0 + _window->createWinId(); + window()->setScreen(activeWindowScreen); +#endif // Qt < 6.0.0 + DEBUG_LOG(("Viewer Pos: New actual screen: %1") + .arg(screenList.indexOf(_window->screen()))); + } + updateGeometry(inMove); +} + +void OverlayWidget::initFullScreen() { + if (_fullscreenInited) { + return; + } + _fullscreenInited = true; + switch (Core::App().settings().mediaViewPosition().maximized) { + case 2: + _fullscreen = true; + _windowed = false; + break; + case 1: + _fullscreen = Platform::IsMac(); + _windowed = false; + break; + } +} + +void OverlayWidget::initNormalGeometry() { + if (_normalGeometryInited) { + return; + } + _normalGeometryInited = true; + const auto saved = Core::App().settings().mediaViewPosition(); + const auto adjusted = Core::AdjustToScale(saved, u"Viewer"_q); + const auto initial = DefaultPosition(); + _normalGeometry = Window::CountInitialGeometry( + _window, + adjusted, + initial, + { st::mediaviewMinWidth, st::mediaviewMinHeight }, + u"Viewer"_q); +} + +void OverlayWidget::savePosition() { + if (isHidden() || isMinimized() || !_normalGeometryInited) { + return; + } + const auto &savedPosition = Core::App().settings().mediaViewPosition(); + auto realPosition = savedPosition; + if (_fullscreen) { + realPosition.maximized = 2; + realPosition.moncrc = 0; + DEBUG_LOG(("Viewer Pos: Saving fullscreen position.")); + } else if (!_windowed) { + realPosition.maximized = 1; + realPosition.moncrc = 0; + DEBUG_LOG(("Viewer Pos: Saving maximized position.")); + } else if (!_wasWindowedMode) { + return; + } else { + auto r = _normalGeometry = _window->body()->mapToGlobal( + _window->body()->rect()); + realPosition.x = r.x(); + realPosition.y = r.y(); + realPosition.w = r.width(); + realPosition.h = r.height(); + realPosition.scale = cScale(); + realPosition.maximized = 0; + realPosition.moncrc = 0; + DEBUG_LOG(("Viewer Pos: " + "Saving non-maximized position: %1, %2, %3, %4" + ).arg(realPosition.x + ).arg(realPosition.y + ).arg(realPosition.w + ).arg(realPosition.h)); + } + realPosition = Window::PositionWithScreen( + realPosition, + _window, + { st::mediaviewMinWidth, st::mediaviewMinHeight }, + u"Viewer"_q); + if (realPosition.w >= st::mediaviewMinWidth + && realPosition.h >= st::mediaviewMinHeight + && realPosition != savedPosition) { + DEBUG_LOG(("Viewer Pos: " + "Writing: %1, %2, %3, %4 (scale %5%, maximized %6)") + .arg(realPosition.x) + .arg(realPosition.y) + .arg(realPosition.w) + .arg(realPosition.h) + .arg(realPosition.scale) + .arg(Logs::b(realPosition.maximized))); + Core::App().settings().setMediaViewPosition(realPosition); + Core::App().saveSettingsDelayed(); + } +} + +void OverlayWidget::updateGeometry(bool inMove) { + initFullScreen(); + if (_fullscreen) { + updateGeometryToScreen(inMove); + } else if (_windowed && _normalGeometryInited) { + const auto setGeometry = [=](QRect geometry) { + DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4") + .arg(geometry.x()) + .arg(geometry.y()) + .arg(geometry.width()) + .arg(geometry.height())); + _window->setGeometry(geometry); + }; + const auto geometry = _normalGeometry; + setGeometry(geometry); + if constexpr (Platform::IsMac()) { + // Either macOS or Qt immediately overwrite the geometry for + // some time (perhaps until the window is really on screen), + // try to set again later in event loop + InvokeQueued(_window, [=] { + setGeometry(geometry); + }); + } + } + if constexpr (!Platform::IsMac()) { + if (_fullscreen) { + if (!isHidden() && !isMinimized()) { + _window->showFullScreen(); + } + } else if (!_windowed) { + if (!isHidden() && !isMinimized()) { + _window->showMaximized(); + } + } + } +} + +void OverlayWidget::updateGeometryToScreen(bool inMove) { + // When the last monitor is removed QGuiApplication has no screens at + // all, so screen() is nullptr. + const auto screen = _window->screen(); + if (!screen) { + return; + } + const auto available = screen->geometry(); + if (_window->geometry() == available) { + return; + } + DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4") + .arg(available.x()) + .arg(available.y()) + .arg(available.width()) + .arg(available.height())); + _window->Ui::RpWidget::setGeometry(available); +} + +void OverlayWidget::updateControlsGeometry() { + updateNavigationControlsGeometry(); + + _saveMsg.moveTo( + (width() - _saveMsg.width()) / 2, + _minUsedTop + (_maxUsedHeight - _saveMsg.height()) / 2); + _photoRadialRect = QRect( + QPoint( + (width() - st::radialSize.width()) / 2, + _minUsedTop + (_maxUsedHeight - st::radialSize.height()) / 2), + st::radialSize); + + const auto bottom = st::mediaviewShadowBottom.height(); + const auto top = st::mediaviewShadowTop.size(); + _bottomShadowRect = QRect(0, height() - bottom, width(), bottom); + _topShadowRect = QRect( + QPoint(topShadowOnTheRight() ? (width() - top.width()) : 0, 0), + top); + + if (_dropdown && !_dropdown->isHidden()) { + _dropdown->moveToRight(0, height() - _dropdown->height()); + } + + updateControls(); + updateTtlBadgePosition(); + resizeContentByScreenSize(); + update(); +} + +void OverlayWidget::refreshTtlBadge(TimeId destroyAt) { + const auto media = _message ? _message->media() : nullptr; + const auto singleView = media + && media->ttlSecondsSingleView() + && _message->isTtlCoveredMedia(); + const auto item = ((destroyAt > 0 || singleView) && _message) + ? _message->fullId() + : FullMsgId(); + if (_ttlBadgeItem == item && _ttlBadgeDestroyAt == destroyAt) { + return; + } + _ttlBadgeItem = item; + _ttlBadgeDestroyAt = destroyAt; + _ttlTooltip = nullptr; + if (!item) { + _ttlBadge = nullptr; + return; + } + if (singleView) { + _ttlBadge = Ui::MakeTtlOnceBadge(_body); + const auto isVideo = (media->document() != nullptr); + auto text = _message->out() + ? (isVideo + ? tr::lng_ttl_video_tooltip_out + : tr::lng_ttl_photo_tooltip_out)( + lt_user, + rpl::single( + _message->history()->peer->shortName() + ) | rpl::map(tr::rich), + tr::rich) + : (isVideo + ? tr::lng_ttl_video_tooltip_in + : tr::lng_ttl_photo_tooltip_in)(tr::rich); + _ttlTooltip = std::make_unique( + _body, + object_ptr>( + _body, + Ui::MakeTtlTooltipContent(_body, std::move(text)), + st::defaultImportantTooltip.padding), + st::dialogsStoriesTooltip); + _ttlTooltip->toggleFast(true); + } else { + const auto ttl = media ? media->ttlSeconds() : crl::time(); + _ttlBadge = Ui::MakeTtlCountdownBadge(_body, destroyAt, ttl); + } + updateTtlBadgePosition(); +} + +void OverlayWidget::updateTtlBadgePosition() { + if (!_ttlBadge) { + return; + } + const auto skip = st::mediaviewTtlSkip; + const auto margin = st::ttlMediaBadgeMargin; + const auto left = topShadowOnTheRight() + ? (skip - margin) + : (width() - _ttlBadge->width() - skip + margin); + _ttlBadge->move(left, _minUsedTop + skip - margin); + _ttlBadge->raise(); + if (_ttlTooltip) { + const auto badge = QRect( + _ttlBadge->pos() + QPoint(margin, margin), + Size(st::ttlMediaBadgeSize)); + const auto tooltipSkip = st::mediaviewTtlTooltipSkip; + _ttlTooltip->pointAt(badge, RectPart::Bottom, [=](QSize size) { + return QPoint( + std::max( + std::min( + rect::right(badge) - size.width(), + width() - skip - size.width()), + skip), + rect::bottom(badge) + tooltipSkip); + }); + _ttlTooltip->raise(); + } +} + +void OverlayWidget::markTimedMediaRead() { + const auto item = _message; + if (!item || !item->isTtlCoveredMedia()) { + return; + } else if (item->isIncomingUnreadMedia()) { + item->history()->session().api().markContentsRead(item); + } +} + +void OverlayWidget::checkSingleViewMediaBurn() { + const auto item = _message; + if (!item || item->out()) { + return; + } + const auto media = item->media(); + if (media + && media->ttlSecondsSingleView() + && item->isTtlCoveredMedia()) { + item->clearMediaAsExpired(); + } +} + +void OverlayWidget::updateNavigationControlsGeometry() { + _minUsedTop = topNotchSkip(); + _maxUsedHeight = height() - _minUsedTop; + + const auto overRect = QRect( + QPoint(), + QSize(st::mediaviewIconOver, st::mediaviewIconOver)); + const auto navSize = _stories + ? st::storiesControlSize + : st::mediaviewControlSize; + const auto navSkip = st::mediaviewHeaderTop; + const auto xLeft = _stories ? (_x - navSize) : 0; + const auto xRight = _stories ? (_x + _w) : (width() - navSize); + _leftNav = QRect( + xLeft, + _minUsedTop + navSkip, + navSize, + _maxUsedHeight - 2 * navSkip); + _leftNavOver = _stories + ? QRect() + : style::centerrect(_leftNav, overRect); + _leftNavIcon = style::centerrect( + _leftNav, + _stories ? st::storiesLeft : st::mediaviewLeft); + _rightNav = QRect( + xRight, + _minUsedTop + navSkip, + navSize, + _maxUsedHeight - 2 * navSkip); + _rightNavOver = _stories + ? QRect() + : style::centerrect(_rightNav, overRect); + _rightNavIcon = style::centerrect( + _rightNav, + _stories ? st::storiesRight : st::mediaviewRight); +} + +bool OverlayWidget::topShadowOnTheRight() const { + return _topShadowRight.current(); +} + +QSize OverlayWidget::flipSizeByRotation(QSize size) const { + return FlipSizeByRotation(size, _rotation); +} + +bool OverlayWidget::hasCopyMediaRestriction(bool skipPremiumCheck) const { + if (const auto story = _stories ? _stories->story() : nullptr) { + if (story->call()) { + return true; + } + return skipPremiumCheck + ? !story->canDownloadIfPremium() + : !story->canDownloadChecked(); + } + return (_history && !_history->peer->allowsForwarding()) + || (_message && _message->forbidsSaving()); +} + +bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) { + if (!hasCopyMediaRestriction(skipPRemiumCheck)) { + return false; + } else if (_stories) { + uiShow()->showToast(tr::lng_error_nocopy_story(tr::now)); + } else if (_history) { + uiShow()->showToast(_history->peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : _history->peer->isUser() + ? tr::lng_error_nocopy_user(tr::now) + : tr::lng_error_nocopy_group(tr::now)); + } + return true; +} + +bool OverlayWidget::videoShown() const { + return _streamed + && _streamed->ready + && !_streamed->instance.info().video.cover.isNull(); +} + +QSize OverlayWidget::videoSize() const { + Expects(videoShown()); + + const auto use = (_document && _chosenQuality != _document) + ? _chosenQuality->dimensions + : _streamed->instance.info().video.size; + return flipSizeByRotation(use); +} + +bool OverlayWidget::streamingRequiresControls() const { + return !_stories + && _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(style::DevicePixelRatio()); + if (_flip) { + image = image.mirrored(_flip & Qt::Horizontal, _flip & Qt::Vertical); + } + _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) { + finishSystemMediaControls(); + 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 (_message + && _message->forbidsSaving() + && _documentMedia->loaded(true)) { + redisplayContent(); + } else if ((_document->loading() && _docCancel->isHidden()) + || (!_document->loading() && !_docCancel->isHidden())) { + updateControls(); + } else if (_document->loading()) { + updateDocSize(); + _widget->update(_docRect); + } + } else if (_streamed && _streamed->controls) { + const auto ready = _documentMedia->loaded() + ? _document->size + : _document->loading() + ? std::clamp(_document->loadOffset(), int64(), _document->size) + : 0; + _streamed->controls->setLoadingProgress(ready, _document->size); + } + if (_stories + && !_documentLoadingTo.isEmpty() + && _document->location(true).isEmpty()) { + showSaveMsgToast( + base::take(_documentLoadingTo), + tr::lng_mediaview_video_saved_to); + } +} + +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 (_stories) { + _leftNavVisible = _stories->subjumpAvailable(-1); + _rightNavVisible = _stories->subjumpAvailable(1); + } else if (_instantViewMediaData) { + _leftNavVisible = _index && (*_index > 0); + _rightNavVisible = _index + && (*_index + 1 < _instantViewMediaData->size()); + } else 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::computeSaveButtonVisible() const { + if (hasCopyMediaRestriction(true)) { + 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::showPremiumDownloadPromo() { + const auto filter = [=](const auto &...) { + if (const auto window = uiShow()->resolveWindow()) { + ShowPremiumPreviewBox(window, PremiumFeature::Stories); + window->window().activate(); + } + return false; + }; + uiShow()->showToast({ + .text = tr::lng_stories_save_promo( + tr::now, + lt_link, + tr::link( + tr::bold( + tr::lng_send_as_premium_required_link(tr::now))), + tr::marked), + .filter = filter, + .adaptive = true, + .duration = kStorySavePromoDuration, + }); +} + +void OverlayWidget::updateControls() { + if (_document && documentBubbleShown()) { + _docRect = QRect( + (width() - st::mediaviewFileSize.width()) / 2, + _minUsedTop + (_maxUsedHeight - 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, + _minUsedTop + (_maxUsedHeight - st::mediaviewFileIconSize) / 2, + st::mediaviewFileIconSize, + st::mediaviewFileIconSize); + _docDownload->hide(); + _docSaveAs->hide(); + _docCancel->hide(); + } + radialStart(); + + updateThemePreviewGeometry(); + + const auto story = _stories ? _stories->story() : nullptr; + const auto overRect = QRect( + QPoint(), + QSize(st::mediaviewIconOver, st::mediaviewIconOver)); + _saveVisible = computeSaveButtonVisible(); + _shareVisible = story && story->canShare(); + _rotateVisible = !_themePreviewShown && !story; + _drawVisible = _drawButtonEnabled + && !_themePreviewShown + && !story + && !(_message + && _message->media() + && _message->media()->ttlSeconds()) + && (_photo || (_document && _document->isImage())); + _recognizeVisible = _recognitionResult.success + && !_recognitionResult.items.empty(); + const auto navRect = [&](int i) { + return QRect( + width() - st::mediaviewIconSize.width() * i, + height() - st::mediaviewIconSize.height(), + st::mediaviewIconSize.width(), + st::mediaviewIconSize.height()); + }; + auto index = 1; + _moreNav = navRect(index); + _moreNavOver = style::centerrect(_moreNav, overRect); + _moreNavIcon = style::centerrect(_moreNav, st::mediaviewMore); + ++index; + _rotateNav = navRect(index); + _rotateNavOver = style::centerrect(_rotateNav, overRect); + _rotateNavIcon = style::centerrect(_rotateNav, st::mediaviewRotate); + if (_rotateVisible) { + ++index; + } + _shareNav = navRect(index); + _shareNavOver = style::centerrect(_shareNav, overRect); + _shareNavIcon = style::centerrect(_shareNav, st::mediaviewShare); + if (_shareVisible) { + ++index; + } + _saveNav = navRect(index); + _saveNavOver = style::centerrect(_saveNav, overRect); + _saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave); + if (_saveVisible) { + ++index; + } + _drawNav = navRect(index); + _drawNavOver = style::centerrect(_drawNav, overRect); + _drawNavIcon = style::centerrect(_drawNav, st::mediaviewDraw); + if (_drawVisible) { + ++index; + } + _recognizeNav = navRect(index); + _recognizeNavOver = style::centerrect(_recognizeNav, overRect); + _recognizeNavIcon = style::centerrect(_recognizeNav, st::mediaviewRecognize); + Assert(st::mediaviewSave.size() == st::mediaviewSaveLocked.size()); + + 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 = d.isValid() ? Ui::FormatDateTime(d) : QString(); + const auto destroyAt = _message ? _message->mediaDestroyAt() : TimeId(); + if (destroyAt > 0) { + const auto left = std::max( + destroyAt - base::unixtime::now(), + TimeId(0)); + _dateText += u" ยท %1"_q.arg(tr::lng_mediaview_ttl_in( + tr::now, + lt_time, + tr::lng_seconds_tiny(tr::now, lt_count, left))); + _ttlTimer.callOnce(crl::time(1000)); + } else { + _ttlTimer.cancel(); + } + refreshTtlBadge(destroyAt); + if (!_fromName.isEmpty()) { + _fromNameLabel.setText( + st::mediaviewTextStyle, + _fromName, + Ui::NameTextOptions()); + _nameNav = QRect( + st::mediaviewTextLeft, + height() - st::mediaviewTextTop, + std::min(_fromNameLabel.maxWidth(), width() / 3), + st::mediaviewFont->height); + const auto separatorWidth = st::mediaviewFont->width(Ui::kQBullet); + _separatorNav = QRect( + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf, + height() - st::mediaviewTextTop, + separatorWidth, + st::mediaviewFont->height); + _dateNav = QRect( + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf + + separatorWidth + + st::mediaviewTextSkipHalf, + height() - st::mediaviewTextTop, + st::mediaviewFont->width(_dateText), + st::mediaviewFont->height); + } else { + _nameNav = QRect(); + _separatorNav = 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(); + refreshSponsoredButtonGeometry(); + refreshVoteButton(); + refreshPollVotersWidget(); + refreshVoteButtonGeometry(); + refreshPollVotersWidgetGeometry(); + refreshCaptionGeometry(); + refreshSponsoredButtonWidth(); + + _pollUpdateLifetime.destroy(); + if (currentPollAnswer()) { + const auto media = _message ? _message->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + if (poll && _session) { + _session->data().pollUpdates( + ) | rpl::filter([=](not_null updated) { + return updated == poll; + }) | rpl::on_next([=] { + refreshVoteButton(); + refreshPollVotersWidget(); + refreshVoteButtonGeometry(); + refreshPollVotersWidgetGeometry(); + refreshCaptionGeometry(); + }, _pollUpdateLifetime); + } + } +} + +void OverlayWidget::refreshCaptionGeometry() { + _caption.updateSkipBlock(0, 0); + _captionShowMoreWidth = 0; + _captionSkipBlockWidth = 0; + + const auto storiesCaptionWidth = _w + - st::mediaviewCaptionPadding.left() + - st::mediaviewCaptionPadding.right(); + if (_caption.isEmpty() && (!_stories || !_stories->repost())) { + _captionRect = QRect(); + return; + } else if (_fullScreenVideo) { + _captionRect = QRect(); + return; + } + + if (_groupThumbs && _groupThumbs->hiding()) { + _groupThumbs = nullptr; + _groupThumbsRect = QRect(); + } + const auto captionBottom = _stories + ? (_y + _h) + : _sponsoredButton + ? (_sponsoredButton->y() - st::mediaviewCaptionMargin.height()) + : _voteButton + ? (_voteButton->y() - st::mediaviewCaptionMargin.height()) + : _pollVotersWidget + ? (_pollVotersWidget->y() - st::mediaviewCaptionMargin.height()) + : (_streamed && _streamed->controls) + ? (_streamed->controls->y() - st::mediaviewCaptionMargin.height()) + : _groupThumbs + ? _groupThumbsTop + : height() - st::mediaviewCaptionMargin.height(); + const auto captionWidth = _stories + ? storiesCaptionWidth + : std::min( + (_groupThumbsAvailableWidth + - st::mediaviewCaptionPadding.left() + - st::mediaviewCaptionPadding.right()), + _caption.maxWidth()); + const auto lineHeight = st::mediaviewCaptionStyle.font->height; + const auto wantedHeight = _caption.countHeight(captionWidth); + const auto maxHeight = !_stories + ? (_maxUsedHeight / 4) + : (wantedHeight > lineHeight * Stories::kMaxShownCaptionLines) + ? (lineHeight * Stories::kCollapsedCaptionLines) + : wantedHeight; + const auto captionHeight = std::min( + wantedHeight, + (maxHeight / lineHeight) * lineHeight); + if (_stories && captionHeight < wantedHeight) { + const auto padding = st::storiesShowMorePadding; + _captionShowMoreWidth = st::storiesShowMoreFont->width( + tr::lng_stories_show_more(tr::now)); + _captionSkipBlockWidth = _captionShowMoreWidth + + padding.left() + + padding.right() + - st::mediaviewCaptionPadding.right(); + const auto skiph = st::storiesShowMoreFont->height + + padding.bottom() + - st::mediaviewCaptionPadding.bottom(); + _caption.updateSkipBlock(_captionSkipBlockWidth, skiph); + } + _captionRect = QRect( + (width() - captionWidth) / 2, + (captionBottom + - captionHeight + - st::mediaviewCaptionPadding.bottom()), + captionWidth, + captionHeight); +} + +void OverlayWidget::refreshSponsoredButtonGeometry() { + if (!_sponsoredButton) { + return; + } + const auto controllerBottom = (_groupThumbs && !_fullScreenVideo) + ? _groupThumbsTop + : height(); + const auto captionRect = captionGeometry(); + _sponsoredButton->resize( + captionRect.width(), + _sponsoredButton->height()); + _sponsoredButton->move( + (width() - captionRect.width()) / 2, + (controllerBottom // Duplicated in recountSkipTop(). + - ((_streamed && _streamed->controls) + ? (_streamed->controls->height() + + st::mediaviewCaptionPadding.bottom()) + : 0) + - _sponsoredButton->height() + - st::mediaviewCaptionMargin.height())); + Ui::SendPendingMoveResizeEvents(_sponsoredButton.get()); +} + +void OverlayWidget::refreshSponsoredButtonWidth() { + if (!_sponsoredButton) { + return; + } + const auto captionWidth = captionGeometry().width(); + _sponsoredButton->resize(captionWidth, _sponsoredButton->height()); + _sponsoredButton->move( + (width() - captionWidth) / 2, + _sponsoredButton->y()); +} + +const PollAnswer *OverlayWidget::currentPollAnswer() const { + if (!_message || !_session) { + return nullptr; + } + const auto media = _message->media(); + if (!media) { + return nullptr; + } + const auto poll = media->poll(); + if (!poll || (!_photo && !_document)) { + return nullptr; + } + const auto current = _photo + ? WebPageCollage::Item(_photo) + : WebPageCollage::Item(_document); + for (const auto &answer : poll->answers) { + const auto candidate = PollAnswerMediaItem(answer); + if (candidate && (*candidate == current)) { + return &answer; + } + } + return nullptr; +} + +void OverlayWidget::refreshVoteButton() { + const auto answer = currentPollAnswer(); + const auto media = _message ? _message->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + const auto show = answer + && poll + && !answer->chosen + && !poll->closed() + && !poll->voted() + && poll->sendingVotes.empty(); + if (!show) { + if (_voteButton) { + _voteButton.destroy(); + } + return; + } + if (!_voteButton) { + _voteButton.create( + _body, + tr::lng_polls_submit_votes(), + st::mediaviewVoteButton); + const auto effect = Ui::CreateChild( + _voteButton.data()); + effect->setOpacity(_controlsOpacity.current()); + _voteButton->setGraphicsEffect(effect); + _voteButton->setClickedCallback([=] { + if (!_session) { + return; + } + const auto current = currentPollAnswer(); + if (!current) { + return; + } + _session->api().polls().sendVotes( + _message->fullId(), + { current->option }); + _voteButton.destroy(); + refreshCaptionGeometry(); + }); + } + _voteButton->show(); +} + +void OverlayWidget::refreshVoteButtonGeometry() { + if (!_voteButton) { + return; + } + const auto controllerBottom = (_groupThumbs && !_fullScreenVideo) + ? _groupThumbsTop + : height(); + _voteButton->moveToLeft( + (width() - _voteButton->width()) / 2, + controllerBottom + - ((_streamed && _streamed->controls) + ? (_streamed->controls->height() + + st::mediaviewCaptionPadding.bottom()) + : 0) + - _voteButton->height() + - st::mediaviewVoteButtonMargin); +} + +void OverlayWidget::refreshPollVotersWidget() { + const auto answer = currentPollAnswer(); + const auto show = answer + && (answer->votes > 0) + && !_voteButton; + if (!show) { + _pollVotersWidget.destroy(); + return; + } + + _pollVotersWidget.destroy(); + _pollVotersWidget.create(_body); + + const auto effect = Ui::CreateChild( + _pollVotersWidget.data()); + effect->setOpacity(_controlsOpacity.current()); + _pollVotersWidget->setGraphicsEffect(effect); + + auto prepared = HistoryView::PrepareUserpicsInRow( + answer->recentVoters, + st::mediaviewPollVotersUserpics, + 3); + + const auto votesText = tr::lng_polls_votes_count( + tr::now, + lt_count, + answer->votes); + const auto &font = st::semiboldFont; + const auto textWidth = font->width(votesText); + + const auto userpicsWidth = prepared.width; + const auto spacing = (userpicsWidth > 0) + ? st::mediaviewPollVotersSpacing + : 0; + const auto &padding = st::mediaviewPollVotersPadding; + const auto widgetWidth = padding.left() + + userpicsWidth + + spacing + + textWidth + + padding.right(); + const auto widgetHeight = st::mediaviewVoteButton.height; + + _pollVotersWidget->resize(widgetWidth, widgetHeight); + + const auto userpicSize = st::mediaviewPollVotersUserpics.size; + const auto raw = _pollVotersWidget.data(); + raw->paintRequest() | rpl::on_next([=, + image = std::move(prepared.image)] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + + const auto radius = st::mediaviewCaptionRadius; + p.setPen(Qt::NoPen); + p.setBrush(st::mediaviewCaptionBg); + p.drawRoundedRect(raw->rect(), radius, radius); + + auto left = padding.left(); + + if (!image.isNull()) { + const auto top = (widgetHeight - userpicSize) / 2; + p.drawImage(left, top, image); + left += userpicsWidth + spacing; + } + + p.setFont(font); + p.setPen(st::mediaviewCaptionFg); + const auto textTop = (widgetHeight + font->ascent - font->descent) / 2; + p.drawText(left, textTop, votesText); + }, raw->lifetime()); + + _pollVotersWidget->show(); +} + +void OverlayWidget::refreshPollVotersWidgetGeometry() { + if (!_pollVotersWidget) { + return; + } + const auto controllerBottom = (_groupThumbs && !_fullScreenVideo) + ? _groupThumbsTop + : height(); + _pollVotersWidget->moveToLeft( + (width() - _pollVotersWidget->width()) / 2, + controllerBottom + - ((_streamed && _streamed->controls) + ? (_streamed->controls->height() + + st::mediaviewCaptionPadding.bottom()) + : 0) + - _pollVotersWidget->height() + - st::mediaviewVoteButtonMargin); +} + +void OverlayWidget::fillContextMenuActions( + const Ui::Menu::MenuCallback &addAction) { + if (_message && _message->isSponsored()) { + if (const auto window = findWindow()) { + const auto show = window->uiShow(); + const auto fullId = _message->fullId(); + Menu::FillSponsored( + addAction, + show, + fullId, + { .dark = true, .skipInfo = true }); + } + return; + } + if (_message) { + const auto media = _message->media(); + const auto invoice = media ? media->invoice() : nullptr; + if (invoice + && invoice->isPaidMedia + && invoice->currency == Ui::kCreditsCurrency + && !invoice->extendedMedia.empty() + && invoice->amount > 0) { + addAction( + Lang::FormatCountDecimal(invoice->amount), + [] {}, + &st::mediaMenuIconStar); + } + } + const auto story = _stories ? _stories->story() : nullptr; + if (!story && _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 (currentPollAnswer()) { + const auto media = _message ? _message->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + if (poll + && poll->voted() + && !poll->closed() + && !poll->quiz() + && !poll->revotingDisabled()) { + const auto itemId = _message->fullId(); + addAction(tr::lng_polls_retract(tr::now), [=] { + if (_session) { + _session->api().polls().sendVotes(itemId, {}); + } + }, &st::mediaMenuIconRetractVote); + } + } + if (story && story->peer()->isSelf()) { + const auto inProfile = story->inProfile(); + const auto text = inProfile + ? tr::lng_mediaview_archive_story(tr::now) + : tr::lng_mediaview_save_to_profile(tr::now); + addAction(text, [=] { + if (_stories) { + _stories->toggleInProfileRequested(!inProfile); + } + }, (inProfile + ? &st::mediaMenuIconArchiveStory + : &st::mediaMenuIconSaveStory)); + } + if ((!story || story->canDownloadChecked()) + && _document + && !_document->filepath(true).isEmpty() + && !(_message && _message->forbidsSaving())) { + 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); + } + const auto hasRecognitionSelection = _recognition.hasSelection(); + if (hasRecognitionSelection) { + addAction( + tr::lng_context_copy_selected(tr::now), + [=] { copyRecognitionSelection(); }, + &st::mediaMenuIconCopy); + } + if (!hasRecognitionSelection && !hasCopyMediaRestriction()) { + if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) { + addAction( + ((_document && _streamed) + ? tr::lng_mediaview_copy_frame(tr::now) + : 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); + if (canShareAtTime()) { + const auto now = [=] { + return tr::lng_mediaview_share_at_time( + tr::now, + lt_time, + Stories::FormatShareAtTime(shareAtVideoTimestamp())); + }; + const auto action = addAction( + now(), + [=] { shareAtTime(); }, + &st::mediaMenuIconShare); + struct State { + rpl::variable text; + rpl::lifetime lifetime; + }; + const auto state = Ui::CreateChild(action); + state->text = rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(120) + ) | rpl::map(now); + state->text.changes() | rpl::on_next([=](QString value) { + action->setText(value); + }, state->lifetime); + } + } + if (story && story->canShare()) { + addAction(tr::lng_mediaview_forward(tr::now), [=] { + _stories->shareRequested(); + }, &st::mediaMenuIconForward); + } + const auto canDelete = [&] { + if (story && story->canDelete()) { + return true; + } else 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(true)) { + addAction( + tr::lng_mediaview_save_as(tr::now), + [=] { saveAs(); }, + (saveControlLocked() + ? &st::mediaMenuIconDownloadLocked + : &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::on_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 (const auto user = _peer->asUser()) { + if (user->hasPersonalPhoto() + && user->userpicPhotoId() == _photo->id) { + 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); + }(); + { + const auto channel = story ? story->peer()->asChannel() : nullptr; + using Flag = ChannelDataFlag; + if (channel && (channel->flags() & Flag::CanGetStatistics)) { + const auto peer = channel; + const auto fullId = story->fullId(); + addAction(tr::lng_stats_title(tr::now), [=] { + if (const auto window = findWindow()) { + close(); + using namespace Info; + window->showSection(Statistics::Make(peer, {}, fullId)); + } + }, &st::mediaMenuIconStats); + } + } + if (_stories + && _stories->allowStealthMode() + && story + && story->peer()->isUser() + && !story->call()) { + const auto now = base::unixtime::now(); + const auto stealth = _session->data().stories().stealthMode(); + addAction(tr::lng_stealth_mode_menu_item(tr::now), [=] { + _stories->setupStealthMode(); + }, ((_session->premium() || (stealth.enabledTill > now)) + ? &st::mediaMenuIconStealth + : &st::mediaMenuIconStealthLocked)); + } + if (story && story->canReport()) { + addAction(tr::lng_profile_report(tr::now), [=] { + _stories->reportRequested(); + }, &st::mediaMenuIconReport); + } +} + +auto OverlayWidget::computeOverviewType() const +-> std::optional { + if (_instantViewMediaData) { + return std::nullopt; + } + 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); + } + if (_sponsoredButton) { + const auto value = _controlsOpacity.current(); + _sponsoredButton->setOpacity(value); + _sponsoredButton->setAttribute( + Qt::WA_TransparentForMouseEvents, + value < 1); + } + if (_voteButton) { + const auto value = _controlsOpacity.current(); + if (const auto e = _voteButton->graphicsEffect()) { + static_cast(e)->setOpacity(value); + } + _voteButton->setAttribute( + Qt::WA_TransparentForMouseEvents, + value < 1); + } + if (_pollVotersWidget) { + const auto value = _controlsOpacity.current(); + if (const auto e = _pollVotersWidget->graphicsEffect()) { + static_cast(e)->setOpacity(value); + } + _pollVotersWidget->setAttribute( + Qt::WA_TransparentForMouseEvents, + value < 1); + } + _helper->setControlsOpacity(_controlsOpacity.current()); + const auto content = finalContentRect(); + const auto siblingType = (_over == Over::LeftStories) + ? Stories::SiblingType::Left + : Stories::SiblingType::Right; + const auto toUpdate = QRegion() + + (_over == Over::Left ? _leftNavOver : _leftNavIcon) + + (_over == Over::Right ? _rightNavOver : _rightNavIcon) + + (_over == Over::Save ? _saveNavOver : _saveNavIcon) + + (_over == Over::Share ? _shareNavOver : _shareNavIcon) + + (_over == Over::Rotate ? _rotateNavOver : _rotateNavIcon) + + (_over == Over::More ? _moreNavOver : _moreNavIcon) + + (_over == Over::Draw ? _drawNavOver : _drawNavIcon) + + (_over == Over::Recognize + ? _recognizeNavOver + : _recognizeNavIcon) + + ((_stories + && (_over == Over::LeftStories || _over == Over::RightStories)) + ? _stories->sibling(siblingType).layout.geometry + : QRect()) + + _headerNav + + _nameNav + + _separatorNav + + _dateNav + + _captionRect.marginsAdded(st::mediaviewCaptionPadding) + + _groupThumbsRect + + content.intersected(_bottomShadowRect) + + content.intersected(_topShadowRect); + update(toUpdate); + return (dt < 1); +} + +void OverlayWidget::waitingAnimationCallback() { + if (!anim::Disabled()) { + update(radialRect()); + } +} + +void OverlayWidget::updateCursor() { + setCursor((_clickHandlerActive || _clickHandlerPressed) + ? style::cur_pointer + : (_controlsState == ControlsHidden) + ? Qt::BlankCursor + : (_over == Over::None || (_over == Over::Video && _stories)) + ? 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 { + if (_stories) { + auto result = storiesContentGeometry(_stories->contentLayout()); + if (!_caption.isEmpty()) { + result.bottomShadowSkip = _widget->height() + - _captionRect.y() + + st::mediaviewCaptionStyle.font->height + - st::storiesShadowBottom.height(); + } + return result; + } + const auto controlsOpacity = _controlsOpacity.current(); + const auto toRotation = qreal(finalContentRotation()); + const auto toRectRotated = QRectF(finalContentRect()); + const auto toRectCenter = rect::center(toRectRotated); + 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, controlsOpacity }; + } + 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, controlsOpacity }; +} + +OverlayWidget::ContentGeometry OverlayWidget::storiesContentGeometry( + const Stories::ContentLayout &layout, + float64 scale) const { + return { + .rect = QRectF(layout.geometry), + .controlsOpacity = kStoriesControlsOpacity, + .fade = layout.fade, + .scale = scale, + .roundRadius = layout.radius, + .topShadowShown = !layout.headerOutside, + }; +} + +void OverlayWidget::updateContentRect() { + if (_opengl) { + update(); + } else { + update(finalContentRect()); + } +} + +void OverlayWidget::contentSizeChanged() { + _width = _w; + _height = _h; + resizeContentByScreenSize(); +} + +void OverlayWidget::recountSkipTop() { + const auto controllerBottomNoFullScreenVideo = _groupThumbs + ? _groupThumbsTop + : height(); + // We need the bottom in case of non-full-screen-video mode + // to count correct _availableHeight in non-full-screen-video mode. + // + // Originally this is controls->y() - padding.bottom(). + const auto bottom = (_streamed && _streamed->controls) + ? (controllerBottomNoFullScreenVideo + - _streamed->controls->height() + - 2 * st::mediaviewCaptionPadding.bottom()) + : height(); + const auto skipHeightBottom = (height() - bottom); + _skipTop = _minUsedTop + std::min( + std::max( + st::mediaviewCaptionMargin.height(), + height() - _height - skipHeightBottom), + skipHeightBottom); + _availableHeight = height() - skipHeightBottom - _skipTop; + if (_fullScreenVideo && skipHeightBottom > 0 && _width > 0) { + const auto h = width() * _height / _width; + const auto topAllFit = _maxUsedHeight - skipHeightBottom - h; + if (_skipTop > topAllFit) { + _skipTop = std::max(topAllFit, 0); + } + } +} + +void OverlayWidget::resizeContentByScreenSize() { + if (_stories) { + const auto content = _stories->finalShownGeometry(); + _x = content.x(); + _y = content.y(); + _w = content.width(); + _h = content.height(); + _zoom = 0; + updateNavigationControlsGeometry(); + if (_videoStream) { + _videoStream->updateGeometry(_x, _y, _w, _h); + } + return; + } else if (_videoStream) { + _w = _body->width() / 2; + _h = _body->height() / 2; + _x = (_body->width() - _w) / 2; + _y = (_body->height() - _h) / 2; + _videoStream->updateGeometry(_x, _y, _w, _h); + return; + } + recountSkipTop(); + const auto availableWidth = width(); + 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(), _maxUsedHeight); + } else { + _zoomToDefault = _zoomToScreen = 0; + } + const auto usew = _fullScreenVideo ? width() : availableWidth; + const auto useh = _fullScreenVideo ? _maxUsedHeight : _availableHeight; + if ((_width > usew) || (_height > useh) || _fullScreenVideo) { + const auto use = _fullScreenVideo ? _zoomToScreen : _zoomToDefault; + _zoom = kZoomToScreenLevel; + if (use >= 0) { + _w = int(base::SafeRound(_width * (use + 1))); + _h = int(base::SafeRound(_height * (use + 1))); + } else { + _w = int(base::SafeRound(_width / (-use + 1))); + _h = int(base::SafeRound(_height / (-use + 1))); + } + } else { + _zoom = 0; + _w = _width; + _h = _height; + } + _x = (width() - _w) / 2; + _y = _fullScreenVideo + ? (_minUsedTop + (_maxUsedHeight - _h) / 2) + : (_skipTop + (useh - _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(); + 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(std::optional anchor) { + auto newZoom = _zoom; + const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault; + if (newZoom == kZoomToScreenLevel) { + if (int(std::ceil(full)) <= kMaxZoomLevel) { + newZoom = int(std::ceil(full)); + } + } else { + if (newZoom < full && (newZoom + 1 > full || (full > kMaxZoomLevel && newZoom == kMaxZoomLevel))) { + newZoom = kZoomToScreenLevel; + } else if (newZoom < kMaxZoomLevel) { + ++newZoom; + } + } + zoomUpdate(newZoom, anchor); +} + +void OverlayWidget::zoomOut(std::optional anchor) { + auto newZoom = _zoom; + const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault; + if (newZoom == kZoomToScreenLevel) { + if (int(std::floor(full)) >= -kMaxZoomLevel) { + newZoom = int(std::floor(full)); + } + } else { + if (newZoom > full && (newZoom - 1 < full || (full < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) { + newZoom = kZoomToScreenLevel; + } else if (newZoom > -kMaxZoomLevel) { + --newZoom; + } + } + zoomUpdate(newZoom, anchor); +} + +void OverlayWidget::zoomReset() { + if (_stories || _fullScreenVideo) { + return; + } + auto newZoom = _zoom; + const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault; + if (_zoom == 0) { + if (int(std::floor(full)) == int(std::ceil(full)) + && int(base::SafeRound(full)) >= -kMaxZoomLevel + && int(base::SafeRound(full)) <= kMaxZoomLevel) { + newZoom = int(base::SafeRound(full)); + } else { + newZoom = kZoomToScreenLevel; + } + } else { + newZoom = 0; + } + _x = -_width / 2; + _y = _skipTop - (_height / 2); + float64 z = (_zoom == kZoomToScreenLevel) ? full : _zoom; + if (z >= 0) { + _x = int(base::SafeRound(_x * (z + 1))); + _y = int(base::SafeRound(_y * (z + 1))); + } else { + _x = int(base::SafeRound(_x / (-z + 1))); + _y = int(base::SafeRound(_y / (-z + 1))); + } + _x += width() / 2; + _y += _availableHeight / 2; + update(); + zoomUpdate(newZoom); +} + +void OverlayWidget::zoomUpdate( + int32 &newZoom, + std::optional anchor) { + if (newZoom != kZoomToScreenLevel) { + while ((newZoom < 0 && (-newZoom + 1) > _w) || (-newZoom + 1) > _h) { + ++newZoom; + } + } + setZoomLevel(newZoom, false, anchor); +} + +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; + _instantViewMedia = nullptr; + _instantViewMediaData = std::nullopt; + _collage = nullptr; + _session = nullptr; +} + +OverlayWidget::~OverlayWidget() { + clearSession(); + + // Otherwise dropdownHidden() may be called from the destructor. + _dropdown.destroy(); +} + +void OverlayWidget::assignMediaPointer(DocumentData *document) { + _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _flip = {}; + _photo = nullptr; + _photoMedia = nullptr; + _videoStream = nullptr; + if (_document != document) { + _streamedQualityChangeFrame = QImage(); + _streamedQualityChangeFinished = false; + if ((_document = document)) { + _quality = _document->initialPlaybackVideoQuality( + Core::App().settings().videoQuality()); + _chosenQuality = _document->chooseQuality(_message, _quality); + _documentMedia = _document->createMediaView(); + _videoCover = LookupVideoCover(_document, _message); + if (_videoCover) { + _videoCoverMedia = _videoCover->createMediaView(); + _videoCoverMedia->wanted( + Data::PhotoSize::Large, + fileOrigin()); + } else { + _videoCoverMedia = nullptr; + _documentMedia->goodThumbnailWanted(); + _documentMedia->thumbnailWanted(fileOrigin()); + } + } else { + _chosenQuality = nullptr; + _documentMedia = nullptr; + _videoCover = nullptr; + _videoCoverMedia = nullptr; + } + _documentLoadingTo = QString(); + } +} + +void OverlayWidget::assignMediaPointer(not_null photo) { + _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _chosenQuality = nullptr; + _streamedQualityChangeFrame = QImage(); + _streamedQualityChangeFinished = false; + _document = nullptr; + _documentMedia = nullptr; + _documentLoadingTo = QString(); + _videoCover = nullptr; + _videoCoverMedia = nullptr; + _videoStream = nullptr; + if (_photo != photo) { + _flip = {}; + _photo = photo; + _photoMedia = _photo->createMediaView(); + _photoMedia->wanted(Data::PhotoSize::Small, fileOrigin()); + if (!_photo->hasVideo() || _photo->videoPlaybackFailed()) { + _photo->load(fileOrigin(), LoadFromCloudOrLocal, true); + } + } +} + +void OverlayWidget::assignMediaPointer( + std::shared_ptr call) { + _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _chosenQuality = nullptr; + _streamedQualityChangeFrame = QImage(); + _streamedQualityChangeFinished = false; + _document = nullptr; + _documentMedia = nullptr; + _documentLoadingTo = QString(); + _videoCover = nullptr; + _videoCoverMedia = nullptr; + _flip = {}; + _photo = nullptr; + _photoMedia = nullptr; + + _videoStream = nullptr; + _videoStream = std::make_unique( + _body, + _surface.get(), + _opengl, + _wrap->backend(), + uiShow(), + std::move(call), + _callLinkSlug, + _callJoinMessageId); + _videoStream->closeRequests() | rpl::on_next([=] { + close(); + }, _videoStream->lifetime()); + if (const auto stories = _stories.get()) { + stories->updateVideoStream(_videoStream->call()); + _videoStream->toggleCommentsOn(stories->commentsShownValue()); + } + _widget->lower(); +} + +void OverlayWidget::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { + _clickHandlerActive = active; + updateCursor(); + update(QRegion(_saveMsg) + captionGeometry()); +} + +void OverlayWidget::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + _clickHandlerPressed = pressed; + updateCursor(); + update(QRegion(_saveMsg) + captionGeometry()); +} + +rpl::lifetime &OverlayWidget::lifetime() { + return _surface->lifetime(); +} + +void OverlayWidget::showSaveMsgFile() { + File::ShowInFolder(_saveMsgFilename); +} + +void OverlayWidget::close() { + if (isHidden()) { + return; + } + hide(); + _helper->clearState(); +} + +void OverlayWidget::minimize() { + if (isHidden()) { + return; + } + _helper->minimize(_window); +} + +void OverlayWidget::toggleFullScreen() { + toggleFullScreen(!_fullscreen); +} + +void OverlayWidget::toggleFullScreen(bool fullscreen) { + _helper->clearState(); + _fullscreen = fullscreen; + _windowed = !fullscreen; + initNormalGeometry(); + if constexpr (Platform::IsMac()) { + _helper->beforeShow(_fullscreen); + updateGeometry(); + _helper->afterShow(_fullscreen); + } else if (_fullscreen) { + updateGeometry(); + _window->showFullScreen(); + } else { + _wasWindowedMode = false; + _window->showNormal(); + updateGeometry(); + _wasWindowedMode = true; + } + savePosition(); + _helper->clearState(); +} + +void OverlayWidget::activateControls() { + if (!_menu && !_mousePressed && !_touchMove && !_stories) { + _controlsHideTimer.callOnce(st::mediaviewWaitHide); + } + if (_fullScreenVideo) { + if (_streamed && _streamed->controls) { + _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 (_stories) { + _controlsState = ControlsShown; + _controlsOpacity = anim::value(1); + _helper->setControlsOpacity(1.); + return; + } else if (!force) { + if (!_dropdown->isHidden() + || (_streamed + && _streamed->controls + && _streamed->controls->hasMenu()) + || _menu + || _mousePressed + || _touchMove) { + return; + } + } + if (_fullScreenVideo && _streamed && _streamed->controls) { + _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(); + if (_stories) { + _stories->menuShown(false); + } + _ignoringDropdown = true; + _lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos()); + updateOver(_lastMouseMovePos); + _ignoringDropdown = false; + if (!_controlsHideTimer.isActive()) { + hideControls(true); + } +} + +void OverlayWidget::handleScreenChanged(not_null 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 = _window->screen(); + if (!screen) { + return; + } + base::qt_signal_producer( + screen, + &QScreen::geometryChanged + ) | rpl::filter([=] { + return !isHidden() && !isMinimized() && _fullscreen; + }) | rpl::on_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) { + _helper->notifyFileDialogShown(shown); +} + +void OverlayWidget::saveAs() { + if (showCopyMediaRestriction(true)) { + return; + } else if (hasCopyMediaRestriction()) { + Assert(_stories != nullptr); + showPremiumDownloadPromo(); + 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); + if (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( + _window.get(), + tr::lng_save_video(tr::now), + filter, + filedialogDefaultName( + u"photo"_q, + u".mp4"_q, + QString(), + false, + _photo->date()), + crl::guard(_window, [=](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( + _window.get(), + tr::lng_save_photo(tr::now), + filter, + filedialogDefaultName( + u"photo"_q, + u".jpg"_q, + QString(), + false, + _photo->date()), + crl::guard(_window, [=](const QString &result) { + if (!result.isEmpty() && _photo == photo) { + media->saveToFile(result); + } + })); + } + activate(); +} + +void OverlayWidget::handleDocumentClick() { + if (_document->loading()) { + saveCancel(); + } else { + _reShow = true; + Data::ResolveDocument( + findWindow(), + _document, + _message, + _topicRootId, + _monoforumPeerId, + _drawButtonEnabled); + if (_document && _document->loading() && !_radial.animating()) { + _radial.start(_documentMedia->progress()); + } + _reShow = false; + } +} + +bool OverlayWidget::canShareAtTime() const { + const auto media = _message ? _message->media() : nullptr; + return _document + && media + && _streamed + && (_document == media->document()) + && _document->isVideoFile() + && !media->webpage(); +} + +TimeId OverlayWidget::shareAtVideoTimestamp() const { + return _streamedPosition / crl::time(1000); +} + +void OverlayWidget::shareAtTime() { + if (!canShareAtTime()) { + return; + } + if (!_streamed->instance.player().paused() + && !_streamed->instance.player().finished()) { + playbackPauseResume(); + } + const auto show = uiShow(); + const auto timestamp = shareAtVideoTimestamp(); + show->show(Stories::PrepareShareAtTimeBox(show, _message, timestamp)); +} + +void OverlayWidget::downloadMedia() { + if (!_photo && !_document) { + return; + } else if (Core::App().settings().askDownloadPath()) { + return saveAs(); + } else if (hasCopyMediaRestriction()) { + if (_stories && !hasCopyMediaRestriction(true)) { + showPremiumDownloadPromo(); + } + return; + } + + 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( + base::FileNameFromUserString(_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()); + } + } + if (_stories && !toName.isEmpty()) { + showSaveMsgToast(toName, tr::lng_mediaview_video_saved_to); + } + location.accessDisable(); + } else { + if (_document->filepath(true).isEmpty() + && !_document->loading()) { + const auto document = _document; + const auto checkSaveStarted = [=] { + if (isHidden() || _document != document) { + return; + } + _documentLoadingTo = _document->loadingFilePath(); + if (_stories && _documentLoadingTo.isEmpty()) { + const auto toName = _document->filepath(true); + if (!toName.isEmpty()) { + showSaveMsgToast( + toName, + tr::lng_mediaview_video_saved_to); + } + } + }; + DocumentSaveClickHandler::SaveAndTrack( + _message ? _message->fullId() : FullMsgId(), + _document, + DocumentSaveClickHandler::Mode::ToFile, + crl::guard(_widget, checkSaveStarted)); + } else { + _saveVisible = computeSaveButtonVisible(); + update(_saveNavOver); + } + 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 = computeSaveButtonVisible(); + update(_saveNavOver); + } 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()) { + showSaveMsgToast(toName, (_stories && _document) + ? tr::lng_mediaview_video_saved_to + : tr::lng_mediaview_saved_to); + } +} + +void OverlayWidget::saveCancel() { + if (_document && _document->loading()) { + _document->cancel(); + if (_documentMedia->canBePlayed()) { + redisplayContent(); + } + } +} + +void OverlayWidget::showInFolder() { + if (!_document) return; + + auto filepath = _document->filepath(true); + if (!filepath.isEmpty()) { + File::ShowInFolder(filepath); + if (!_windowed) { + 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) { + if (!_windowed) { + close(); + } + Window::ShowForwardMessagesBox(active.front(), { 1, id }); + } +} + +void OverlayWidget::deleteMedia() { + if (_stories) { + _stories->deleteRequested(); + return; + } else 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 list = HistoryItemsList{ message }; + if (CanCreateModerateMessagesBox(list)) { + const auto opt = DefaultModerateMessagesBoxOptions(); + window->show( + Box( + CreateModerateMessagesBox, + ModerateMessagesBoxEntry{ .items = list }, + nullptr, + opt), + Ui::LayerOption::CloseOther); + } else { + window->show( + Box(message), + Ui::LayerOption::CloseOther); + } + } + } +} + +void OverlayWidget::showMediaOverview() { + if (_menu) { + _menu->hideMenu(true); + } + update(); + if (const auto overviewType = computeOverviewType()) { + if (!_windowed) { + close(); + } + if (SharedMediaOverviewType(*overviewType)) { + if (const auto window = findWindow()) { + const auto topic = _topicRootId + ? _history->peer->forumTopicFor(_topicRootId) + : nullptr; + const auto sublist = _monoforumPeerId + ? _history->peer->monoforumSublistFor(_monoforumPeerId) + : nullptr; + if (_topicRootId && !topic) { + return; + } else if (_monoforumPeerId && !sublist) { + return; + } + window->showSection(_topicRootId + ? std::make_shared( + topic, + Info::Section(*overviewType)) + : _monoforumPeerId + ? std::make_shared( + sublist, + Info::Section(*overviewType)) + : std::make_shared( + _history->peer, + Info::Section(*overviewType))); + } + } + } +} + +void OverlayWidget::recognize() { + _showRecognitionResults = !_showRecognitionResults; + if (!_showRecognitionResults) { + clearRecognitionSelection(); + } + _recognitionAnimation.start( + [=] { update(); }, + _showRecognitionResults ? 0. : 1., + _showRecognitionResults ? 1. : 0., + st::widgetFadeDuration); +} + +void OverlayWidget::draw() { + if (!_session) { + return; + } + const auto photoId = _photo ? _photo->id : PhotoId(); + const auto documentId = (_document && _document->isImage()) + ? _document->id + : DocumentId(); + if (!photoId && !documentId) { + return; + } + _session->data().requestDrawToReply({ + _message ? _message->fullId() : FullMsgId(), + photoId, + documentId, + }); + close(); +} + +void OverlayWidget::copyMedia() { + if (showCopyMediaRestriction()) { + return; + } + _dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow); + if (_document) { + const auto filepath = _document->filepath(true); + auto image = transformedShownContent(); + if (!image.isNull() || !filepath.isEmpty()) { + auto mime = std::make_unique(); + if (!image.isNull()) { + mime->setImageData(std::move(image)); + } + if (!filepath.isEmpty() && !videoShown()) { + mime->setUrls({ QUrl::fromLocalFile(filepath) }); + KUrlMimeData::exportUrlsToPortal(mime.get()); + } + QGuiApplication::clipboard()->setMimeData(mime.release()); + } + } else if (_photo && _photoMedia->loaded()) { + _photoMedia->setToClipboard(); + } +} + +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; + } + if (!_windowed) { + close(); + } +} + +auto OverlayWidget::sharedMediaType() const +-> std::optional { + using Type = SharedMediaType; + if (_message) { + if (const auto media = _message->media()) { + if (media->webpage() || media->invoice()) { + return std::nullopt; + } else if (const auto poll = media->poll()) { + const auto isPollMedia = [&](const auto &item) { + return IsPollAnswerMediaItem(poll, item) + || (PollInputMediaItem(poll->solutionMedia) + == item); + }; + if ((_photo + && isPollMedia(WebPageCollage::Item(_photo))) + || (_document + && isPollMedia( + WebPageCollage::Item(_document)))) { + 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 + PeerId(0), // monoforumPeerId + _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), + (isScheduled ? PeerId() : _monoforumPeerId), + _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 (_stories) { + return _stories->fileOrigin(); + } else if (_message) { + return _message->fullId(); + } else if (_photo && _user) { + const auto dominated = (_user->hasPersonalPhoto() + && _photo->id == _user->userpicPhotoId()) + || SyncUserFallbackPhotoViewer(_user) == _photo->id; + if (dominated) { + return Data::FileOriginFullUser(peerToUser(_user->id)); + } + 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) { + const auto dominated = (_user->hasPersonalPhoto() + && photo->id == _user->userpicPhotoId()) + || SyncUserFallbackPhotoViewer(_user) == photo->id; + if (dominated) { + return Data::FileOriginFullUser(peerToUser(_user->id)); + } + 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::on_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::on_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); +} + +auto OverlayWidget::instantViewMediaKey() const +-> std::optional { + if (_photo) { + return InstantViewItem(_photo); + } else if (_document) { + return InstantViewItem(_document); + } + return std::nullopt; +} + +bool OverlayWidget::validInstantViewMedia() const { + if (const auto key = instantViewMediaKey()) { + if (!_instantViewMedia || (_instantViewMedia->item != _message)) { + return false; + } + if (*key == _instantViewMedia->key) { + return true; + } else if (_instantViewMediaData) { + const auto &items = *_instantViewMediaData; + if (ranges::find(items, *key) != end(items) + && ranges::find(items, _instantViewMedia->key) != end(items)) { + return true; + } + } + } + return (_instantViewMedia == nullptr); +} + +void OverlayWidget::validateInstantViewMedia() { + if (_message) { + if (const auto media = _message->Get()) { + if (const auto key = instantViewMediaKey()) { + const auto i = ranges::find(media->items, *key); + if (i != end(media->items)) { + _instantViewMedia = std::make_unique( + _message, + *key); + _instantViewMediaData = media->items; + return; + } + } + } + } + _instantViewMedia = nullptr; + _instantViewMediaData = std::nullopt; +} + +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; + } + } + } else if (const auto invoice = media->invoice()) { + for (const auto &item : invoice->extendedMedia) { + if (_photo && item->photo() == _photo) { + return _photo; + } else if (_document && item->document() == _document) { + return _document; + } + } + } else if (const auto poll = media->poll()) { + if (_photo + && IsPollAnswerMediaItem( + poll, + WebPageCollage::Item(_photo))) { + return _photo; + } else if (_document + && IsPollAnswerMediaItem( + poll, + WebPageCollage::Item(_document))) { + return _document; + } + } + } + } + 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 if (const auto invoice = media->invoice()) { + auto &data = *_collageData; + data.items.reserve(invoice->extendedMedia.size()); + for (const auto &item : invoice->extendedMedia) { + if (const auto photo = item->photo()) { + data.items.push_back(photo); + } else if (const auto document = item->document()) { + data.items.push_back(document); + } + } + } else if (const auto poll = media->poll()) { + _collageData = PollAnswersCollage(poll); + } + } + } + } else { + _collage = nullptr; + _collageData = std::nullopt; + } +} + +void OverlayWidget::refreshMediaViewer() { + if (!validInstantViewMedia()) { + validateInstantViewMedia(); + } + if (_instantViewMediaData) { + _sharedMedia = nullptr; + _sharedMediaData = std::nullopt; + _sharedMediaDataKey = std::nullopt; + } else if (!validSharedMedia()) { + validateSharedMedia(); + } + if (!validUserPhotos()) { + validateUserPhotos(); + } + if (!validCollage()) { + validateCollage(); + } + findCurrent(); + updateControls(); +} + +void OverlayWidget::refreshFromLabel() { + if (_message) { + _from = _message->originalSender(); + if (const auto info = _message->originalHiddenSenderInfo()) { + _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(); + const auto caption = [&] { + if (_stories) { + return StripQuoteEntities(_stories->captionText()); + } else if (_message) { + if (const auto caption = InstantViewMediaCaption( + _message, + _photo, + _document)) { + return *caption; + } + if (const auto media = _message->media()) { + if (media->webpage()) { + if (_message->isSponsored()) { + return TextWithEntities() + .append(tr::bold(media->webpage()->title)) + .append('\n') + .append(media->webpage()->description); + } + return TextWithEntities(); + } else if (const auto poll = media->poll()) { + const auto current = _photo + ? WebPageCollage::Item(_photo) + : WebPageCollage::Item(_document); + for (const auto &answer : poll->answers) { + const auto candidate = PollAnswerMediaItem(answer); + if (candidate && (*candidate == current)) { + return answer.text; + } + } + const auto solution = PollInputMediaItem( + poll->solutionMedia); + if (solution && (*solution == current)) { + return poll->solution; + } + return TextWithEntities(); + } + } + return _message->translatedText(); + } + return TextWithEntities(); + }(); + if (caption.text.isEmpty()) { + if (_streamed && _streamed->controls) { + _streamed->controls->setTimestamps({}); + refreshClipControllerGeometry(); + } + return; + } + + using namespace HistoryView; + _caption = Ui::Text::String(st::msgMinWidth); + const auto duration = (_streamed && _document && _message) + ? 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::TextContext({ + .session = (_stories + ? _storiesSession + : &_message->history()->session()), + .repaint = captionRepaint, + }); + auto captionText = base.isEmpty() + ? caption + : AddTimestampLinks(caption, duration, base); + refreshTimestampDividers(captionText, duration); + _caption.setMarkedText( + st::mediaviewCaptionStyle, + std::move(captionText), + (_message + ? Ui::ItemTextOptions(_message) + : Ui::ItemTextDefaultOptions()), + context); + if (_caption.hasSpoilers()) { + const auto weak = base::make_weak(widget()); + _caption.setSpoilerLinkFilter([=](const ClickContext &context) { + return (weak != nullptr); + }); + } + if (_caption.hasCollapsedBlockquots()) { + _caption.setBlockquoteExpandCallback(crl::guard(_widget, [=]( + int index, + bool expanded) { + const auto wasGeometry = captionGeometry(); + refreshCaptionGeometry(); + update(wasGeometry.united(captionGeometry())); + })); + } +} + +void OverlayWidget::refreshTimestampDividers( + const TextWithEntities &caption, + TimeId duration) { + if (!_streamed || !_streamed->controls || duration <= 0) { + return; + } + using Data = PlaybackControls::TimestampData; + auto timestamps = std::vector(); + for (const auto &entity : caption.entities) { + if (entity.type() != EntityType::CustomUrl + || !entity.data().startsWith( + u"internal:media_timestamp"_q)) { + continue; + } + const auto tPos = entity.data().lastIndexOf(u"&t="_q); + if (tPos < 0) { + continue; + } + auto ok = false; + const auto time = entity.data().mid(tPos + 3).toInt(&ok); + if (!ok || time < 0 || time > duration) { + continue; + } + const auto offset = entity.offset(); + const auto atLineStart = (offset == 0) + || (caption.text[offset - 1] == '\n'); + auto label = QString(); + if (atLineStart) { + const auto entityEnd = offset + entity.length(); + const auto nl = caption.text.indexOf('\n', entityEnd); + const auto lineEnd = (nl >= 0) + ? nl + : caption.text.size(); + label = caption.text.mid( + entityEnd, + lineEnd - entityEnd).trimmed(); + if (label.startsWith(u"- "_q)) { + label = label.mid(2).trimmed(); + } else if (label.startsWith(u"-"_q)) { + label = label.mid(1).trimmed(); + } + } + timestamps.push_back({ + .position = float64(time) / duration, + .label = std::move(label), + }); + } + _streamed->controls->setTimestamps(std::move(timestamps)); + refreshClipControllerGeometry(); +} + +void OverlayWidget::refreshGroupThumbs() { + const auto existed = (_groupThumbs != nullptr); + if (_index && _instantViewMediaData) { + const auto messageId = _message ? _message->fullId() : FullMsgId(); + View::GroupThumbs::Refresh( + _session, + _groupThumbs, + { messageId, &*_instantViewMediaData }, + *_index, + _groupThumbsAvailableWidth); + } else 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::on_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::on_next([this](View::GroupThumbs::Key key) { + using CollageKey = View::GroupThumbs::CollageKey; + using InstantViewKey = View::GroupThumbs::InstantViewKey; + 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)); + } + } else if (const auto instantViewKey = std::get_if(&key)) { + if (_instantViewMediaData) { + moveToEntity(entityForInstantViewMedia(instantViewKey->index)); + } + } + }, _groupThumbs->lifetime()); + + _groupThumbsRect = QRect( + _groupThumbsLeft, + _groupThumbsTop, + width() - 2 * _groupThumbsLeft, + height() - _groupThumbsTop); +} + +void OverlayWidget::clearControlsState() { + _saveMsgAnimation.stop(); + _saveMsgTimer.cancel(); + _loadRequest = 0; + _over = _down = Over::None; + _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 _window->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::isActive() const { + return !isHidden() && !isMinimized() && _window->isActiveWindow(); +} + +bool OverlayWidget::isHidden() const { + return _window->isHidden(); +} + +bool OverlayWidget::isMinimized() const { + return _window->isMinimized(); +} + +bool OverlayWidget::isFullScreen() const { + return _fullscreen; +} + +not_null OverlayWidget::widget() const { + return _widget; +} + +void OverlayWidget::hide() { + clearBeforeHide(); + applyHideWindowWorkaround(); + _window->hide(); + if (Platform::IsWayland()) { + if (const auto handle = _window->windowHandle()) { + handle->destroy(); + } + } +} + +void OverlayWidget::setCursor(style::cursor cursor) { + _widget->setCursor(cursor); +} + +void OverlayWidget::setFocus() { + _body->setFocus(); +} + +bool OverlayWidget::takeFocusFrom(not_null window) const { + return _fullscreen + && !isHidden() + && !isMinimized() + && (_window->screen() == window->screen()); +} + +void OverlayWidget::activate() { + _window->raise(); + _window->activateWindow(); + setFocus(); + QApplication::setActiveWindow(_window); + setFocus(); +} + +void OverlayWidget::show(OpenRequest request) { + const auto story = request.story(); + const auto document = story ? story->document() : request.document(); + const auto photo = story ? story->photo() : request.photo(); + const auto call = story ? story->call() : request.call(); + const auto contextItem = request.item(); + const auto contextPeer = request.peer(); + const auto contextTopicRootId = request.topicRootId(); + const auto contextMonoforumPeerId = request.monoforumPeerId(); + _drawButtonEnabled = request.showDrawButton(); + if (!request.continueStreaming() && !request.startTime() && !_reShow) { + if (_message && (_message == contextItem)) { + return close(); + } else if (_user && (_user == contextPeer)) { + if ((_photo && (_photo == photo)) + || (_document && (_document == document))) { + return close(); + } + } + } + if (isHidden() || isMinimized()) { + // Count top notch on macOS before counting geometry. + _helper->beforeShow(_fullscreen); + } + if (_cachedShow) { + _cachedShow->showOrHideBoxOrLayer( + v::null, + Ui::LayerOption::CloseOther, + anim::type::instant); + } + if (photo) { + if (contextItem && contextPeer) { + return; + } + setSession(&photo->session()); + + if (story) { + setContext(StoriesContext{ + story->peer(), + story->id(), + request.storiesContext(), + }); + } else if (contextPeer) { + setContext(contextPeer); + } else if (contextItem) { + setContext(ItemContext{ + contextItem, + contextTopicRootId, + contextMonoforumPeerId, + }); + } else { + setContext(v::null); + } + + clearControlsState(); + _firstOpenedPeerPhoto = (contextPeer != nullptr); + assignMediaPointer(photo); + + displayPhoto(photo); + preloadData(0); + activateControls(); + } else if (story || document || call) { + setSession(document + ? &document->session() + : story + ? &story->session() + : &call->session()); + + if (story) { + setContext(StoriesContext{ + story->peer(), + story->id(), + request.storiesContext(), + }); + } else if (contextItem) { + setContext(ItemContext{ + contextItem, + contextTopicRootId, + contextMonoforumPeerId, + }); + } else { + setContext(v::null); + } + + clearControlsState(); + + _streamingStartPaused = false; + if (call) { + _callLinkSlug = request.callLinkSlug(); + _callJoinMessageId = request.callJoinMessageId(); + displayVideoStream(call, anim::activation::normal); + } else { + displayDocument( + document, + anim::activation::normal, + (request.cloudTheme() + ? *request.cloudTheme() + : Data::CloudTheme()), + { request.continueStreaming(), request.startTime() }); + } + if (!isHidden()) { + preloadData(0); + activateControls(); + } + } + if (const auto controller = request.controller()) { + _openedFrom = base::make_weak(&controller->window()); + } +} + +void OverlayWidget::displayPhoto( + not_null photo, + anim::activation activation) { + if (photo->isNull()) { + displayDocument(nullptr, activation); + return; + } + _touchbarDisplay.fire(TouchBarItemType::Photo); + + clearStreaming(); + destroyThemePreview(); + + _fullScreenVideo = false; + const auto photoChanged = (_photo != photo); + assignMediaPointer(photo); + _rotation = _photo->owner().mediaRotation().get(_photo); + _radial.stop(); + if (photoChanged) { + _showRecognitionResults = false; + _recognitionResult = {}; + _recognitionPendingSessionUniqueId = 0; + _recognitionPendingPhotoId = 0; + _recognitionPendingDocumentId = 0; + _recognitionRetryOnLarge = false; + clearRecognitionSelection(); + } + + refreshMediaViewer(); + + _staticContent = QImage(); + if (!_stories && _photo->videoCanBePlayed()) { + initStreaming(); + } + + tryStartTextRecognition(); + + initSponsoredButton(); + + refreshCaption(); + + _blurred = true; + _down = Over::None; + if (!_staticContent.isNull()) { + // Video thumbnail. + const auto size = style::ConvertScale( + flipSizeByRotation(_staticContent.size())); + _w = size.width(); + _h = size.height(); + } else { + auto dimensions = QSize(photo->width(), photo->height()); + if (dimensions.isEmpty()) { + const auto best = [&]() -> Image* { + const auto large = _photoMedia->image( + Data::PhotoSize::Large); + const auto thumbnail = _photoMedia->image( + Data::PhotoSize::Thumbnail); + return large + ? large + : thumbnail + ? thumbnail + : _photoMedia->image(Data::PhotoSize::Small); + }(); + if (best) { + dimensions = best->size(); + } + } + const auto size = style::ConvertScale( + flipSizeByRotation(dimensions)); + _w = size.width(); + _h = size.height(); + } + contentSizeChanged(); + refreshFromLabel(); + displayFinished(activation); +} + +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, anim::activation::background); + } else { + displayDocument(_document, anim::activation::background); + } +} + +// Empty messages shown as docs: doc can be nullptr. +void OverlayWidget::displayDocument( + DocumentData *doc, + anim::activation activation, + const Data::CloudTheme &cloud, + const StartStreaming &startStreaming) { + _fullScreenVideo = false; + _staticContent = QImage(); + const auto documentChanged = (_document != doc); + clearStreaming(_document != doc); + destroyThemePreview(); + assignMediaPointer(doc); + + _rotation = _document + ? _document->owner().mediaRotation().get(_document) + : 0; + _themeCloudData = cloud; + _radial.stop(); + if (documentChanged) { + _showRecognitionResults = false; + _recognitionResult = {}; + _recognitionPendingSessionUniqueId = 0; + _recognitionPendingPhotoId = 0; + _recognitionPendingDocumentId = 0; + _recognitionRetryOnLarge = false; + clearRecognitionSelection(); + } + + _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 { + initSponsoredButton(); + if (_documentMedia->canBePlayed() + && 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(); + + tryStartTextRecognition(); + + 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 && _streamed->controls) { + switchToPip(); + } else { + displayFinished(activation); + } +} + +// Empty messages shown as docs: doc can be nullptr. +void OverlayWidget::displayVideoStream( + const std::shared_ptr &call, + anim::activation activation) { + _fullScreenVideo = false; + _staticContent = QImage(); + clearStreaming(true); + destroyThemePreview(); + assignMediaPointer(call); + + _rotation = 0; + _radial.stop(); + + _touchbarDisplay.fire(TouchBarItemType::None); + + contentSizeChanged(); + _blurred = false; + displayFinished(activation); +} + +void OverlayWidget::initSponsoredButton() { + const auto has = _message && _message->isSponsored() && _session; + if (has && _sponsoredButton) { + return; + } else if (!has && _sponsoredButton) { + _sponsoredButton = nullptr; + return; + } else if (!has && !_sponsoredButton) { + return; + } + const auto sponsoredMessages = &_session->sponsoredMessages(); + const auto fullId = _message->fullId(); + const auto details = sponsoredMessages->lookupDetails(fullId); + _sponsoredButton = base::make_unique_q(_body); + _sponsoredButton->setText(details.buttonText); + _sponsoredButton->setOpacity(1.0); + + _sponsoredButton->setClickedCallback([=, link = details.link] { + UrlClickHandler::Open(link); + sponsoredMessages->clicked(fullId, false, true); + hide(); + }); +} + +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 = std::max( + width() - _themePreviewRect.x() - _themePreviewRect.width(), + 0) + + st::themePreviewMargin.right(); + auto bottom = std::min( + 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(anim::activation activation) { + updateControls(); + if (isHidden()) { + _helper->beforeShow(_fullscreen); + moveToScreen(); + showAndActivate(); + } else if (activation == anim::activation::background) { + return; + } else if (isMinimized()) { + _helper->beforeShow(_fullscreen); + showAndActivate(); + } else { + activate(); + } +} + +void OverlayWidget::showAndActivate() { + _body->show(); + initNormalGeometry(); + if (_windowed || Platform::IsMac()) { + _wasWindowedMode = false; + } + updateGeometry(); + if (_windowed || Platform::IsMac()) { + _window->showNormal(); + _wasWindowedMode = true; + } else if (_fullscreen) { + _window->showFullScreen(); + } else { + _window->showMaximized(); + } + _helper->afterShow(_fullscreen); + _widget->update(); + activate(); +} + +bool OverlayWidget::canInitStreaming() const { + return (_document && _documentMedia->canBePlayed()) + || (_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::on_next_error([=](Streaming::Update &&update) { + handleStreamingUpdate(std::move(update)); + }, [=](Streaming::Error &&error) { + handleStreamingError(std::move(error)); + }, _streamed->instance.lifetime()); + + _streamed->instance.switchQualityRequests( + ) | rpl::filter([=](int quality) { + return !_quality.manual && _quality.height != quality; + }) | rpl::on_next([=](int quality) { + applyVideoQuality({ + .manual = 0, + .height = uint32(quality), + }); + }, _streamed->instance.lifetime()); + + const auto continuing = startStreaming.continueStreaming + && _pip + && (_pip->wrapped.shared().get() + == _streamed->instance.shared().get()); + if (startStreaming.continueStreaming) { + _pip = nullptr; + } + if (!continuing + || (!_streamed->instance.player().active() + && !_streamed->instance.player().finished())) { + startStreamingPlayer(startStreaming); + } else { + if (_streamed->instance.player().ready()) { + markStreamedReady(); + } else { + _streamed->ready = false; + } + updatePlaybackState(); + } + return true; +} + +void OverlayWidget::startStreamingPlayer( + const StartStreaming &startStreaming) { + Expects(_streamed != nullptr); + + const auto &player = _streamed->instance.player(); + if (player.playing()) { + if (!_streamed->withSound) { + markStreamedReady(); + return; + } + _pip = nullptr; + } else if (!player.paused() && !player.finished() && !player.failed()) { + _pip = nullptr; + } else if (_pip && _streamed->withSound) { + return; + } + + _streamedPosition = _document + ? startStreaming.startTime + : _photo + ? _photo->videoStartPosition() + : 0; + restartAtSeekPosition(_streamedPosition); +} + +void OverlayWidget::markStreamedReady() { + Expects(_streamed != nullptr); + + if (_streamed->ready) { + return; + } + _streamed->ready = true; + if (const auto sponsored = _streamed->sponsored.get()) { + sponsored->start(); + } +} + +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 = _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Large) + : _document + ? _documentMedia->goodThumbnail() + : _photoMedia->image(Data::PhotoSize::Large); + const auto thumbnail = _videoCover + ? _videoCoverMedia->image(Data::PhotoSize::Small) + : _document + ? _documentMedia->thumbnail() + : computePhotoThumbnail(); + const auto blurred = _videoCover + ? _videoCoverMedia->thumbnailInline() + : _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 (size.isEmpty()) { + return; + } else if (!_streamedQualityChangeFrame.isNull()) { + setStaticContent(_streamedQualityChangeFrame.scaled( + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + return; + } else if (!good && !thumbnail && !blurred) { + 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) { + markStreamedReady(); + if (videoShown()) { + if (_document + && _streamed + && _streamed->ready + && (!_chosenQuality || _chosenQuality == _document)) { + if (const auto video = _document->video()) { + video->realVideoSize = info.video.realSize; + } + } + applyVideoSize(); + _streamedQualityChangeFrame = QImage(); + if (_streamed && _streamed->controls) { + crl::on_main(_widget, [=] { + if (_streamed && _streamed->controls) { + _streamed->controls->updateSpeedToggleQuality(); + } + }); + } + } 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); + Expects(!_streamed); + + const auto origin = fileOrigin(); + const auto callback = [=] { waitingAnimationCallback(); }; + const auto video = _chosenQuality ? _chosenQuality : _document; + if (video) { + _streamed = std::make_unique( + video, + _document, + _message, + origin, + callback); + } else { + _streamed = std::make_unique(_photo, origin, callback); + } + if (!_streamed->instance.valid()) { + _streamed = nullptr; + return false; + } + ++_streamedCreated; + _streamed->instance.setPriority(kOverlayLoaderPriority); + _streamed->instance.lockPlayer(); + _streamed->withSound = video + && !video->isSilentVideo() + && (_document->isAudioFile() + || _document->isVideoFile() + || _document->isVoiceMessage() + || _document->isVideoMessage()); + if (streamingRequiresControls()) { + _streamed->controls = std::make_unique( + _body, + static_cast(this)); + _streamed->controls->show(); + _streamed->sponsored = PlaybackSponsored::Has(_message) + ? std::make_unique( + _streamed->controls.get(), + uiShow(), + _message) + : nullptr; + if (const auto sponsored = _streamed->sponsored.get()) { + _layerBg->layerShownValue( + ) | rpl::on_next([=](bool shown) { + sponsored->setPaused(shown); + }, sponsored->lifetime()); + } + refreshClipControllerGeometry(); + } + 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->windowHandle(); }); +} + +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)); + }, [&](PreloadedVideo) { + updatePlaybackState(); + }, [&](UpdateVideo update) { + updateContentRect(); + Core::App().updateNonIdle(); + updatePlaybackState(); + _streamedPosition = update.position; + }, [&](PreloadedAudio) { + updatePlaybackState(); + }, [&](UpdateAudio) { + updatePlaybackState(); + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { + }, [&](Finished) { + updatePlaybackState(); + if (base::take(_ttlDeferredClose)) { + close(); + } + }); +} + +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(); + current.previewBg = st::themePreviewBg->c; + + 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 = base::make_weak(_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( + _body, + 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( + _body, + tr::lng_cancel(), + st::themePreviewCancelButton); + _themeCancel->show(); + _themeCancel->setClickedCallback([this] { close(); }); + if (const auto slug = _themeCloudData.slug; !slug.isEmpty()) { + _themeShare.create( + _body, + tr::lng_theme_share(), + st::themePreviewCancelButton); + _themeShare->show(); + _themeShare->setClickedCallback([=] { + QGuiApplication::clipboard()->setText( + session->createInternalLinkFull("addtheme/" + slug)); + uiShow()->showToast({ + .text = { + tr::lng_background_link_copied(tr::now), + }, + .iconLottie = u"toast/voip_invite"_q, + .iconLottieSize = st::toastLottieIconSize, + }); + }); + } else { + _themeShare.destroy(); + } + updateControls(); + } + update(); + }); + }); + location.accessDisable(); +} + +void OverlayWidget::refreshClipControllerGeometry() { + if (!_streamed || !_streamed->controls) { + return; + } + + if (_groupThumbs && _groupThumbs->hiding()) { + _groupThumbs = nullptr; + _groupThumbsRect = QRect(); + } + const auto controllerBottom = (_groupThumbs && !_fullScreenVideo) + ? _groupThumbsTop + : height(); + const auto skip = st::mediaviewCaptionPadding.bottom(); + const auto controllerWidth = std::min( + st::mediaviewControllerSize.width(), + width() - 2 * skip); + const auto controllerHeight = st::mediaviewControllerSize.height() + + (_streamed->controls->hasTimestamps() + ? st::mediaviewTimestampLabelHeight + : 0); + _streamed->controls->resize(controllerWidth, controllerHeight); + _streamed->controls->move( + (width() - controllerWidth) / 2, + (controllerBottom // Duplicated in recountSkipTop(). + - _streamed->controls->height() + - st::mediaviewCaptionPadding.bottom())); + Ui::SendPendingMoveResizeEvents(_streamed->controls.get()); +} + +void OverlayWidget::playbackControlsPlay() { + playbackPauseResume(); + activateControls(); +} + +void OverlayWidget::playbackControlsPause() { + playbackPauseResume(); + activateControls(); +} + +void OverlayWidget::playbackControlsToFullScreen() { + playbackToggleFullScreen(); + activateControls(); +} + +void OverlayWidget::playbackControlsFromFullScreen() { + playbackToggleFullScreen(); + activateControls(); +} + +void OverlayWidget::playbackControlsToPictureInPicture() { + if (_streamed && _streamed->controls) { + 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() + || _streamedQualityChangeFinished) { + _streamedQualityChangeFinished = false; + _streamingStartPaused = false; + restartAtSeekPosition(0); + } else if (_streamed->instance.player().paused()) { + _streamed->instance.resume(); + updatePlaybackState(); + playbackPauseMusic(); + } else { + _streamed->instance.pause(); + updatePlaybackState(); + } +} + +void OverlayWidget::flushPendingFrameStep() { + if (!_streamed || !_frameStepPending) { + _frameStepThrottle.cancel(); + return; + } + const auto fps = _streamed->instance.info().video.fps; + const auto stepMs = 1000. + / ((fps > 0.) ? fps : kFrameStepFallbackFps); + const auto shift = crl::time(std::round(_frameStepPending * stepMs)); + _frameStepPending = 0; + _streamingStartPaused = true; + seekRelativeTime(shift); + _frameStepThrottle.callOnce(kFrameStepThrottleMs); +} + +void OverlayWidget::seekRelativeTime(crl::time time) { + Expects(_streamed != nullptr); + + const auto &state = _streamed->instance.info().video.state; + const auto position = state.position; + const auto duration = state.duration; + if (position == kTimeUnknown + || duration == kTimeUnknown + || duration == kDurationUnavailable) { + return; + } + const auto newTime = std::clamp( + position + time, + crl::time(0), + duration); + restartAtSeekPosition(newTime); +} + +void OverlayWidget::restartAtProgress(float64 progress) { + Expects(_streamed != nullptr); + + const auto duration = _streamed->instance.info().video.state.duration; + if (duration == kTimeUnknown + || duration == kDurationUnavailable) { + return; + } + restartAtSeekPosition( + 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(); + } + const auto overrideDuration = _stories + || (_chosenQuality && _chosenQuality != _document); + auto options = Streaming::PlaybackOptions{ + .position = position, + .durationOverride = ((overrideDuration + && _document + && _document->hasDuration()) + ? _document->duration() + : crl::time(0)), + .hwAllowed = Core::App().settings().hardwareAcceleratedVideo(), + .seekable = !_stories, + }; + if (!_streamed->withSound) { + options.mode = Streaming::Mode::Video; + options.loop = !_stories; + } else { + Assert(_document != nullptr); + const auto messageId = _message ? _message->fullId() : FullMsgId(); + options.audioId = AudioMsgId(_document, messageId); + options.speed = _stories + ? 1. + : Core::App().settings().videoPlaybackSpeed(); + if (_pip) { + _pip = nullptr; + } + } + if (_speedBoostActive) { + options.speed = _speedBoostSpeed; + } + _streamed->instance.play(options); + if (_streamingStartPaused) { + _streamed->instance.pause(); + } else { + playbackPauseMusic(); + _streamedQualityChangeFinished = false; + } + _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; + playbackPauseResume(); + } +} + +void OverlayWidget::playbackControlsSeekFinished(crl::time position) { + Expects(_streamed != nullptr); + + _streamingStartPaused = !_streamed->pausedBySeek + && !_streamed->instance.player().finished(); + restartAtSeekPosition(position); + activateControls(); +} + +void OverlayWidget::playbackControlsVolumeChanged(float64 volume) { + if (_streamed) { + Player::mixer()->setVideoVolume(volume); + } else if (_videoStream) { + _videoStream->setVolume(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); + activateControls(); +} + +void OverlayWidget::playbackControlsVolumeChangeFinished() { + const auto volume = Core::App().settings().videoVolume(); + if (volume > 0.) { + _lastPositiveVolume = volume; + } + activateControls(); +} + +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 && _streamed->controls && !_stories) { + DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed)); + _streamed->instance.setSpeed(speed); + } +} + +float64 OverlayWidget::playbackControlsCurrentSpeed(bool lastNonDefault) { + return Core::App().settings().videoPlaybackSpeed(lastNonDefault); +} + +std::vector OverlayWidget::playbackControlsQualities() { + if (!_document) { + return {}; + } + const auto &list = _document->resolveQualities(_message); + if (list.empty()) { + return {}; + } + auto result = std::vector(); + result.reserve(list.size() + 1); + const auto add = [&](not_null quality) { + const auto original = (quality == _document); + const auto height = original + ? quality->resolveOriginalVideoQuality() + : quality->resolveVideoQuality(); + if (!height) { + return; + } + const auto value = VideoQuality{ + .manual = 1u, + .height = uint32(height), + .original = original ? 1u : 0u, + }; + if (!ranges::contains(result, value)) { + result.push_back(value); + } + }; + if (!_document->filepath(true).isEmpty()) { + add(_document); + } + for (const auto &quality : list) { + add(quality); + } + return result; +} + +VideoQuality OverlayWidget::playbackControlsCurrentQuality() { + if (!_chosenQuality) { + return _quality; + } + const auto original = (_chosenQuality == _document); + return { + .manual = _quality.manual, + .height = uint32(original + ? _chosenQuality->resolveOriginalVideoQuality() + : _chosenQuality->resolveVideoQuality()), + .original = original ? 1u : 0u, + }; +} + +void OverlayWidget::playbackControlsQualityChanged(VideoQuality quality) { + applyVideoQuality({ + .manual = uint32(quality.height ? 1 : 0), + .height = quality.height, + .original = quality.original, + }); +} + +void OverlayWidget::applyVideoQuality(VideoQuality value) { + if (_quality == value) { + return; + } + _quality = value; + Core::App().settings().setVideoQuality(value); + Core::App().saveSettingsDelayed(); + + if (!_document) { + return; + } + const auto resolved = _document->chooseQuality(_message, _quality); + if (_chosenQuality == resolved) { + return; + } + _chosenQuality = resolved; + if (_streamed && _streamed->instance.ready()) { + _streamedQualityChangeFrame = currentVideoFrameImage(); + } + if (_streamed + && (!_streamed->instance.player().active() + || _streamed->instance.player().finished())) { + _streamedQualityChangeFinished = true; + } + _streamingStartPaused = _streamedQualityChangeFinished + || (_streamed && _streamed->instance.player().paused()); + const auto wasFullScreen = _fullScreenVideo; + clearStreaming(); + const auto time = _streamedPosition; + const auto startStreaming = StartStreaming(false, time); + if (!canInitStreaming() || !initStreaming(startStreaming)) { + redisplayContent(); + } else { + if (_fullScreenVideo != wasFullScreen) { + _fullScreenVideo = wasFullScreen; + if (_streamed->controls) { + _streamed->controls->setInFullScreen(_fullScreenVideo); + } + } + refreshCaption(); + } +} + +void OverlayWidget::switchToPip() { + Expects(_streamed != nullptr); + Expects(_document != nullptr); + + const auto document = _document; + const auto messageId = _message ? _message->fullId() : FullMsgId(); + const auto topicRootId = _topicRootId; + const auto monoforumPeerId = _monoforumPeerId; + const auto closeAndContinue = [=] { + _showAsPip = false; + show(OpenRequest( + findWindow(false), + document, + document->owner().message(messageId), + topicRootId, + monoforumPeerId, + true)); + }; + _showAsPip = true; + _pip = std::make_unique( + _window, + document, + fileOrigin(), + _chosenQuality ? _chosenQuality : document, + _message, + _quality, + _streamed->instance.shared(), + closeAndContinue, + [=] { _pip = nullptr; }); + + if (const auto raw = _message) { + raw->history()->owner().itemRemoved( + ) | rpl::filter([=](not_null item) { + return (raw == item); + }) | rpl::on_next([=] { + _pip = nullptr; + }, _pip->lifetime); + + Core::App().passcodeLockChanges( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::on_next([=] { + _pip = nullptr; + }, _pip->lifetime); + } + + if (isHidden()) { + clearBeforeHide(); + clearAfterHide(); + } else { + close(); + if (const auto window = Core::App().activeWindow()) { + window->activate(); + } + } +} + +not_null OverlayWidget::storiesWrap() { + return _body; +} + +std::shared_ptr OverlayWidget::storiesShow() { + return uiShow(); +} + +std::shared_ptr OverlayWidget::uiShow() { + if (!_cachedShow) { + _cachedShow = std::make_shared(this); + } + return _cachedShow; +} + +auto OverlayWidget::storiesStickerOrEmojiChosen() +-> rpl::producer { + return _storiesStickerOrEmojiChosen.events(); +} + +void OverlayWidget::storiesJumpTo( + not_null session, + FullStoryId id, + Data::StoriesContext context) { + Expects(_stories != nullptr); + Expects(id.valid()); + + const auto maybeStory = session->data().stories().lookup(id); + if (!maybeStory) { + close(); + return; + } + const auto story = *maybeStory; + setContext(StoriesContext{ + story->peer(), + story->id(), + context, + }); + clearStreaming(); + _streamingStartPaused = false; + v::match(story->media().data, [&](not_null photo) { + displayPhoto(photo, anim::activation::background); + }, [&](not_null document) { + displayDocument(document, anim::activation::background); + }, [&](const std::shared_ptr &call) { + displayVideoStream(call, anim::activation::background); + }, [&](v::null_t) { + displayDocument(nullptr, anim::activation::background); + }); +} + +void OverlayWidget::storiesRedisplay(not_null story) { + Expects(_stories != nullptr); + + clearStreaming(); + _streamingStartPaused = false; + v::match(story->media().data, [&](not_null photo) { + displayPhoto(photo, anim::activation::background); + }, [&](not_null document) { + displayDocument(document, anim::activation::background); + }, [&](const std::shared_ptr &call) { + displayVideoStream(call, anim::activation::background); + }, [&](v::null_t) { + displayDocument(nullptr, anim::activation::background); + }); +} + +void OverlayWidget::storiesClose() { + close(); +} + +bool OverlayWidget::storiesPaused() { + return _streamed + && !_streamed->instance.player().failed() + && !_streamed->instance.player().finished() + && _streamed->instance.player().active() + && _streamed->instance.player().paused(); +} + +rpl::producer OverlayWidget::storiesLayerShown() { + return _layerBg->layerShownValue(); +} + +void OverlayWidget::storiesTogglePaused(bool paused) { + if (!_streamed + || _streamed->instance.player().failed() + || _streamed->instance.player().finished() + || !_streamed->instance.player().active()) { + return; + } else if (_streamed->instance.player().paused()) { + if (!paused) { + _streamed->instance.resume(); + updatePlaybackState(); + playbackPauseMusic(); + } + } else if (paused) { + _streamed->instance.pause(); + updatePlaybackState(); + } +} + +float64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) { + return (type == Stories::SiblingType::Left) + ? overLevel(Over::LeftStories) + : overLevel(Over::RightStories); +} + +void OverlayWidget::storiesRepaint() { + update(); +} + +void OverlayWidget::storiesVolumeToggle() { + playbackControlsVolumeToggled(); +} + +void OverlayWidget::storiesVolumeChanged(float64 volume) { + playbackControlsVolumeChanged(volume); +} + +void OverlayWidget::storiesVolumeChangeFinished() { + playbackControlsVolumeChangeFinished(); +} + +int OverlayWidget::topNotchSkip() const { + return _fullscreen ? _topNotchSize : 0; +} + +int OverlayWidget::storiesTopNotchSkip() { + return topNotchSkip(); +} + +void OverlayWidget::playbackToggleFullScreen() { + Expects(_streamed != nullptr); + + if (_stories + || !videoShown() + || (!_streamed->controls && !_fullScreenVideo)) { + return; + } + _fullScreenVideo = !_fullScreenVideo; + if (_fullScreenVideo) { + _fullScreenZoomCache = _zoom; + } + resizeCenteredControls(); + recountSkipTop(); + setZoomLevel( + _fullScreenVideo ? kZoomToScreenLevel : _fullScreenZoomCache, + true); + if (_streamed->controls) { + if (!_fullScreenVideo) { + _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); + + refreshSystemMediaControls(); + if (!_streamed->controls && !_stories) { + return; + } + const auto state = _streamed->instance.player().prepareLegacyState(); + if (state.position != kTimeUnknown && state.length != kTimeUnknown) { + _streamedPosition = state.position; + if (_streamed->controls) { + _streamed->controls->updatePlayback(state); + _streamed->controls->updateSpeedToggleQuality(); + _touchbarTrackState.fire_copy(state); + updatePowerSaveBlocker(state); + } + if (_stories) { + _stories->updatePlayback(state); + } + } +} + +void OverlayWidget::setSystemMediaControls( + SystemMediaControlsVideoSink *sink) { + _smtcSink = sink; +} + +bool OverlayWidget::contentNeedsScreenshotProtection() const { + if (const auto story = _stories ? _stories->story() : nullptr) { + return story->forbidsForward(); + } + return (_history && !_history->peer->allowsForwarding()) + || (_message && _message->forbidsSaving()); +} + +void OverlayWidget::refreshScreenshotProtection() { + _screenshotProtected = contentNeedsScreenshotProtection(); + Platform::SetWindowScreenshotProtection( + _window, + _screenshotProtected); +} + +void OverlayWidget::refreshSystemMediaControls() { + if (!_smtcSink) { + return; + } + const auto self = static_cast(this); + if (!_streamed || !_streamed->withSound) { + _smtcSink->videoFinish(self); + return; + } + const auto &player = _streamed->instance.player(); + const auto state = player.prepareLegacyState(); + if (state.position == kTimeUnknown || state.length == kTimeUnknown) { + return; + } + const auto channel = _from && _from->isBroadcast(); + const auto filename = _document ? _document->filename() : QString(); + auto title = channel ? _fromName : filename; + if (title.isEmpty()) { + title = _fromName; + } + auto video = SystemMediaControlsVideoSink::VideoState{ + .title = title, + .artist = channel ? filename : _fromName, + .position = std::max(state.position, int64(0)), + .duration = std::max(state.length, int64(0)), + .playing = (!player.paused() && !player.finished()), + .nextAvailable = _rightNavVisible, + .previousAvailable = _leftNavVisible, + }; + if (_smtcDocument != _document) { + _smtcDocument = _document; + _smtcThumbnailSet = false; + _smtcSink->videoStart(self, video); + } else { + _smtcSink->videoUpdate(video); + } + if (!_smtcThumbnailSet) { + const auto cover = systemMediaControlsThumbnail(); + if (!cover.isNull()) { + _smtcSink->videoSetThumbnail(cover); + _smtcThumbnailSet = true; + } + } +} + +QImage OverlayWidget::systemMediaControlsThumbnail() const { + const auto original = [](Image *image) { + return image ? image->original() : QImage(); + }; + auto result = QImage(); + if (_videoCover && _videoCoverMedia) { + result = original(_videoCoverMedia->image(Data::PhotoSize::Large)); + if (result.isNull()) { + result = original( + _videoCoverMedia->image(Data::PhotoSize::Small)); + } + } else if (_documentMedia) { + result = original(_documentMedia->goodThumbnail()); + if (result.isNull()) { + result = original(_documentMedia->thumbnail()); + } + } + if (result.isNull() && videoShown()) { + result = currentVideoFrameImage(); + } + return result; +} + +void OverlayWidget::finishSystemMediaControls() { + if (_smtcSink) { + _smtcSink->videoFinish( + static_cast(this)); + } + _smtcDocument = nullptr; +} + +void OverlayWidget::smtcPlay() { + if (!_streamed) { + return; + } + const auto &player = _streamed->instance.player(); + if (player.paused() || player.finished() || !player.active()) { + playbackPauseResume(); + } +} + +void OverlayWidget::smtcPause() { + if (!_streamed) { + return; + } + const auto &player = _streamed->instance.player(); + if (!player.paused() && !player.finished() && player.active()) { + playbackPauseResume(); + } +} + +void OverlayWidget::smtcPlayPause() { + if (_streamed) { + playbackPauseResume(); + } +} + +void OverlayWidget::smtcStop() { + close(); +} + +void OverlayWidget::smtcNext() { + moveToNext(1); +} + +void OverlayWidget::smtcPrevious() { + moveToNext(-1); +} + +void OverlayWidget::smtcSeek(crl::time position) { + if (!_streamed) { + return; + } + _streamingStartPaused = _streamed->instance.player().paused(); + restartAtSeekPosition(position); + activateControls(); +} + +void OverlayWidget::validatePhotoImage(Image *image, bool blurred) { + if (!image) { + return; + } else if (!_staticContent.isNull() && (blurred || !_blurred)) { + return; + } + const auto use = flipSizeByRotation({ _width, _height }) + * style::DevicePixelRatio(); + setStaticContent(image->pixNoCache( + use, + { .options = (blurred ? Images::Option::Blur : Images::Option()) } + ).toImage()); + _blurred = blurred; +} + +void OverlayWidget::validatePhotoCurrentImage() { + if (!_photo) { + return; + } + if (_width <= 0 || _height <= 0) { + const auto hasImage = _photoMedia->image(Data::PhotoSize::Large) + || _photoMedia->image(Data::PhotoSize::Thumbnail) + || _photoMedia->image(Data::PhotoSize::Small); + if (hasImage && !_photoRedisplayQueued) { + _photoRedisplayQueued = true; + InvokeQueued(_widget, [=] { + _photoRedisplayQueued = false; + redisplayContent(); + }); + } + } + 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()); + } + if (_recognitionRetryOnLarge) { + if (const auto image = _photoMedia->image(Data::PhotoSize::Large) + ; image && !image->original().isNull()) { + tryStartTextRecognition(); + } + } +} + +void OverlayWidget::tryStartTextRecognition() { + if (_stories + || !_session + || !Platform::TextRecognition::IsAvailable()) { + return; + } + const auto forPhoto = (_photo != nullptr) && (_photoMedia != nullptr); + const auto forDocument = !_photo + && _document + && _document->isImage() + && !_staticContent.isNull(); + if (!forPhoto && !forDocument) { + return; + } + const auto cache = RecognitionCache(); + if (!cache) { + return; + } + const auto id = RecognitionId{ + .sessionUniqueId = _session->uniqueId(), + .photoId = forPhoto ? _photo->id : PhotoId(), + .documentId = forDocument ? _document->id : DocumentId(), + }; + if (const auto cached = cache->find(id); cached != cache->end()) { + _recognitionRetryOnLarge = false; + const auto changed = (_recognitionResult.success != cached->second.success) + || (_recognitionResult.items.size() != cached->second.items.size()); + _recognitionResult = cached->second; + if (changed) { + updateControls(); + } + return; + } + if (_recognitionPendingSessionUniqueId == id.sessionUniqueId + && _recognitionPendingPhotoId == id.photoId + && _recognitionPendingDocumentId == id.documentId) { + _recognitionRetryOnLarge = false; + return; + } + auto original = QImage(); + auto recognizeSize = QSize(); + if (forPhoto) { + _photoMedia->wanted(Data::PhotoSize::Large, fileOrigin()); + if (const auto image = _photoMedia->image(Data::PhotoSize::Large)) { + original = image->original(); + } + } else { + const auto ratio = style::DevicePixelRatio(); + const auto percent = style::Scale(); + const auto target = QSize( + (_staticContent.width() * 100) / (ratio * percent), + (_staticContent.height() * 100) / (ratio * percent)); + original = _staticContent; + if (!target.isEmpty() && target != original.size()) { + recognizeSize = target; + } + } + if (original.isNull()) { + _recognitionRetryOnLarge = true; + return; + } + _recognitionRetryOnLarge = false; + _recognitionPendingSessionUniqueId = id.sessionUniqueId; + _recognitionPendingPhotoId = id.photoId; + _recognitionPendingDocumentId = id.documentId; + const auto weak = base::make_weak(_widget); + crl::async([=] { + const auto input = recognizeSize.isEmpty() + ? original + : original.scaled( + recognizeSize, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + auto result = Platform::TextRecognition::RecognizeText(input); + crl::on_main(weak, [=, result = std::move(result)]() mutable { + const auto cache = RecognitionCache(); + if (!cache) { + return; + } + const auto pendingMatches = (_recognitionPendingSessionUniqueId + == id.sessionUniqueId) + && (_recognitionPendingPhotoId == id.photoId) + && (_recognitionPendingDocumentId == id.documentId); + if (pendingMatches) { + _recognitionPendingSessionUniqueId = 0; + _recognitionPendingPhotoId = 0; + _recognitionPendingDocumentId = 0; + } + (*cache)[id] = result; + const auto stillSame = _session + && (_session->uniqueId() == id.sessionUniqueId) + && (id.photoId + ? (_photo && _photo->id == id.photoId) + : (_document && _document->id == id.documentId)); + if (!stillSame) { + return; + } + _recognitionResult = std::move(result); + updateControls(); + }); + }); +} + +Ui::GL::ChosenRenderer OverlayWidget::chooseRenderer( + Ui::GL::Backend backend) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + if (backend == Ui::GL::Backend::QRhi) { + _opengl = true; + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::QRhi, + }; + } +#endif // Qt >= 6.7 + _opengl = (backend == Ui::GL::Backend::OpenGL); + return { + .renderer = (_opengl + ? std::unique_ptr( + std::make_unique(this)) + : std::make_unique(this)), + .backend = backend, + }; +} + +void OverlayWidget::paint(not_null renderer) { + renderer->paintBackground(); + if (contentShown()) { + if (videoShown()) { + renderer->paintTransformedVideoFrame(contentGeometry()); + if (_streamed->instance.player().ready()) { + _streamed->instance.markFrameShown(); + if (_stories) { + _stories->ready(); + } + } + } else { + validatePhotoCurrentImage(); + if (_stories && !_blurred) { + _stories->ready(); + } + const auto fillTransparentBackground = (!_document + || (!_document->sticker() && !_document->isVideoMessage())) + && _staticContentTransparent; + renderer->paintTransformedStaticContent( + _staticContent, + contentGeometry(), + _staticContentTransparent, + fillTransparentBackground); + } + paintRadialLoading(renderer); + if (_stories) { + using namespace Stories; + const auto paint = [&](const SiblingView &view, int index) { + renderer->paintTransformedStaticContent( + view.image, + storiesContentGeometry(view.layout, view.scale), + false, // semi-transparent + false, // fill transparent background + index); + const auto base = (index - 1) * 2; + const auto userpicSize = view.userpic.size() + / view.userpic.devicePixelRatio(); + renderer->paintStoriesSiblingPart( + base, + view.userpic, + QRect(view.userpicPosition, userpicSize)); + const auto nameSize = view.name.size() + / view.name.devicePixelRatio(); + renderer->paintStoriesSiblingPart( + base + 1, + view.name, + QRect(view.namePosition, nameSize), + view.nameOpacity); + }; + if (const auto left = _stories->sibling(SiblingType::Left)) { + paint(left, kLeftSiblingTextureIndex); + } + if (const auto right = _stories->sibling(SiblingType::Right)) { + paint(right, kRightSiblingTextureIndex); + } + } + } else if (_stories) { + if (_videoStream) { + renderer->paintVideoStream(); + } else { + // Unsupported story. + } + } else if (_themePreviewShown) { + renderer->paintThemePreview(_themePreviewRect); + } else if (documentBubbleShown() && !_docRect.isEmpty()) { + renderer->paintDocumentBubble(_docRect, _docIconRect); + } + if (isSaveMsgShown()) { + renderer->paintSaveMsg(_saveMsg); + } + if (isChapterShown()) { + renderer->paintChapter(_chapterRect); + } + if (isSpeedBoostShown()) { + renderer->paintSpeedBoost(_speedBoostRect); + } + + const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current(); + if (opacity > 0) { + paintControls(renderer, opacity); + if (!_stories) { + renderer->paintFooter(footerGeometry(), opacity); + } + if (!(_stories ? _stories->skipCaption() : _caption.isEmpty())) { + renderer->paintCaption(captionGeometry(), opacity); + } + if (_groupThumbs) { + renderer->paintGroupThumbs( + QRect( + _groupThumbsLeft, + _groupThumbsTop, + width() - 2 * _groupThumbsLeft, + _groupThumbs->height()), + opacity); + } + } + checkGroupThumbsAnimation(); + if (const auto radius = _window->manualRoundingRadius()) { + renderer->paintRoundedCorners(radius); + } +} + +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(Over::Icon); + 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.style.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(style::DevicePixelRatio()); + 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::showChapterIndicator( + const QString &name, + int direction) { + if (name.isEmpty()) { + return; + } + _chapterText = name; + + const auto font = st::mediaviewChapterFont; + const auto padding = st::mediaviewChapterPadding; + const auto arrowSpace = st::mediaviewChapterArrowWidth + + st::mediaviewChapterArrowGap; + const auto textWidth = font->width(_chapterText); + const auto w = padding.left() + + arrowSpace + + textWidth + + arrowSpace + + padding.right(); + const auto h = rect::m::sum::v(padding) + font->height; + _chapterRect = QRect( + (width() - w) / 2, + _minUsedTop + (_maxUsedHeight - h) / 2, + w, + h); + + if (isChapterShown()) { + _chapterTimer.callOnce(st::mediaviewChapterShown); + } else { + const auto callback = [=](float64 value) { + updateChapter(); + if (!_chapterAnimation.animating()) { + _chapterTimer.callOnce(st::mediaviewChapterShown); + } + }; + _chapterAnimation.start( + callback, + 0., + 1., + st::mediaviewChapterShowing); + } + + _chapterArrows.erase( + ranges::remove_if( + _chapterArrows, + [&](const auto &a) { + return !a->animation.animating() + || (a->direction != direction); + }), + _chapterArrows.end()); + auto arrow = std::make_unique(); + arrow->direction = direction; + arrow->animation.start( + [=] { updateChapter(); }, + 0., + 1., + st::mediaviewChapterArrowSlide + + st::mediaviewChapterArrowPause + + st::mediaviewChapterArrowFade); + _chapterArrows.push_back(std::move(arrow)); + updateChapter(); +} + +void OverlayWidget::paintChapterContent( + Painter &p, + QRect outer, + QRect clip) { + const auto opacity = _chapterAnimation.value(1.); + p.setOpacity(opacity); + Ui::FillRoundRect( + p, + outer, + st::mediaviewSaveMsgBg, + Ui::MediaviewSaveCorners); + + const auto font = st::mediaviewChapterFont; + const auto padding = st::mediaviewChapterPadding; + const auto arrowSize = st::mediaviewChapterArrowSize; + const auto arrowWidth = st::mediaviewChapterArrowWidth; + const auto arrowGap = st::mediaviewChapterArrowGap; + const auto textX = outer.x() + + padding.left() + + arrowWidth + + arrowGap; + const auto textY = outer.y() + padding.top(); + + p.setFont(font); + p.setPen(st::mediaviewSaveMsgFg); + p.drawText(textX, textY + font->ascent, _chapterText); + + const auto cy = outer.y() + outer.height() / 2.; + const auto halfH = arrowSize / 2.; + const auto totalDuration = float64(st::mediaviewChapterArrowSlide + + st::mediaviewChapterArrowPause + + st::mediaviewChapterArrowFade); + const auto slideEnd = st::mediaviewChapterArrowSlide / totalDuration; + const auto fadeStart = 1. - st::mediaviewChapterArrowFade / totalDuration; + auto hq = PainterHighQualityEnabler(p); + auto pen = QPen(st::mediaviewSaveMsgFg->c); + pen.setWidthF(st::lineWidth * 1.5); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + for (const auto &arrow : _chapterArrows) { + if (!arrow->animation.animating()) { + continue; + } + const auto progress = arrow->animation.value(1.); + const auto slideProgress = std::min(progress / slideEnd, 1.); + const auto shift = st::mediaviewChapterArrowShift * slideProgress; + const auto arrowOpacity = (progress <= fadeStart) + ? slideProgress + : (1. - progress) / (1. - fadeStart); + p.setOpacity(opacity * arrowOpacity); + auto path = QPainterPath(); + if (arrow->direction > 0) { + const auto ax = outer.x() + + outer.width() + - padding.right() + - arrowGap + - arrowWidth + + shift; + path.moveTo(ax, cy - halfH); + path.lineTo(ax + arrowWidth, cy); + path.lineTo(ax, cy + halfH); + } else { + const auto ax = outer.x() + + padding.left() + + arrowGap + + arrowWidth + - shift; + path.moveTo(ax, cy - halfH); + path.lineTo(ax - arrowWidth, cy); + path.lineTo(ax, cy + halfH); + } + p.drawPath(path); + } + p.setOpacity(1); +} + +bool OverlayWidget::isChapterShown() const { + return _chapterAnimation.animating() || _chapterTimer.isActive(); +} + +void OverlayWidget::updateChapter() { + update(_chapterRect); +} + +void OverlayWidget::startSpeedBoost() { + if (_speedBoostActive || !_streamed || _stories) { + return; + } + if (_speedBoostFromMouse && _down != Over::Video) { + return; + } + _speedBoostActive = true; + _speedBoostSavedSpeed = _streamed->instance.speed(); + _speedBoostSpeed = 2.0; + _speedBoostPhase = 0.; + _speedBoostLastFrame = crl::now(); + _streamed->instance.setSpeed(_speedBoostSpeed); + _speedBoostHoldTimer.cancel(); + + updateSpeedBoostRect(); + _speedBoostAnimation.start( + [=] { updateSpeedBoost(); }, + 0., + 1., + st::mediaviewSpeedBoostShowing); + _speedBoostTicker.start(); +} + +void OverlayWidget::stopSpeedBoost() { + if (!_speedBoostActive) { + return; + } + _speedBoostActive = false; + _speedBoostFromMouse = false; + _speedBoostHoldTimer.cancel(); + if (_streamed) { + _streamed->instance.setSpeed(_speedBoostSavedSpeed); + } + _speedBoostAnimation.start( + [=] { updateSpeedBoost(); }, + 1., + 0., + st::mediaviewSpeedBoostHiding); +} + +void OverlayWidget::updateSpeedBoostRect() { + const auto was = _speedBoostRect; + const auto font = st::mediaviewSpeedBoostFont; + const auto padding = st::mediaviewSpeedBoostPadding; + const auto maxText + = QString::fromUtf8("%1\xC3\x97").arg(kSpeedMax, 0, 'f', 1); + const auto arrowStep = std::max( + 1, + st::mediaviewSpeedBoostArrowWidth + + st::mediaviewSpeedBoostArrowGap + - st::mediaviewSpeedBoostArrowOverlap); + const auto arrowSpace = st::mediaviewSpeedBoostArrowWidth + + arrowStep + + st::mediaviewSpeedBoostTextRight; + const auto w = padding.left() + + font->width(maxText) + + arrowSpace + + padding.right(); + const auto h = rect::m::sum::v(padding) + font->height; + _speedBoostRect = QRect( + (width() - w) / 2, + _minUsedTop + st::mediaviewSpeedBoostTop, + w, + h); + if (was != _speedBoostRect && !was.isEmpty()) { + update(was); + } +} + +void OverlayWidget::paintSpeedBoostContent( + Painter &p, + QRect outer, + QRect clip) { + const auto opacity = _speedBoostAnimation.value( + _speedBoostActive ? 1. : 0.); + if (opacity <= 0.) { + return; + } + + const auto scale = 0.6 + 0.4 * opacity; + p.save(); + p.translate(rect::center(outer)); + p.scale(scale, scale); + p.translate(-rect::center(outer)); + + p.setOpacity(opacity); + Ui::FillRoundRect( + p, + outer, + st::mediaviewSaveMsgBg, + Ui::MediaviewSaveCorners); + + const auto font = st::mediaviewSpeedBoostFont; + const auto padding = st::mediaviewSpeedBoostPadding; + const auto text + = QString::fromUtf8("%1\xC3\x97").arg(_speedBoostSpeed, 0, 'f', 1); + const auto arrowWidth = st::mediaviewSpeedBoostArrowWidth; + const auto arrowHeight = st::mediaviewSpeedBoostArrowHeight; + const auto arrowGap = st::mediaviewSpeedBoostArrowGap; + const auto arrowRound = style::ConvertFloatScale(1.5); + const auto arrowStep = std::max( + 1, + arrowWidth + arrowGap - st::mediaviewSpeedBoostArrowOverlap); + const auto arrowsWidth = arrowWidth + arrowStep; + const auto innerArrowWidth = arrowWidth - arrowRound * 2.; + const auto innerArrowHeight = arrowHeight - arrowRound * 2.; + const auto innerHalfH = innerArrowHeight / 2.; + const auto textRight = st::mediaviewSpeedBoostTextRight; + const auto textWidth = font->width(text); + const auto contentWidth = textWidth + textRight + arrowsWidth; + const auto contentX = outer.x() + + (outer.width() - contentWidth) / 2.; + const auto textY = outer.y() + padding.top(); + p.setFont(font); + p.setPen(st::mediaviewSaveMsgFg); + p.drawText(int(contentX), textY + font->ascent, text); + + const auto cy = outer.y() + outer.height() / 2.; + const auto arrowsX = contentX + textWidth + textRight; + const auto now = crl::now(); + const auto dt = std::min( + 0.016, + (now - _speedBoostLastFrame) / 1000.); + _speedBoostLastFrame = now; + _speedBoostPhase += dt * 1.5 * std::min(_speedBoostSpeed, 4.); + + auto hq = PainterHighQualityEnabler(p); + auto stroker = QPainterPathStroker(); + if (arrowRound > 0.) { + stroker.setWidth(arrowRound * 2.); + stroker.setCapStyle(Qt::RoundCap); + stroker.setJoinStyle(Qt::RoundJoin); + } + p.setPen(Qt::NoPen); + for (auto i = 0; i < 2; ++i) { + const auto phase = _speedBoostPhase + i * 0.17; + const auto pulse = std::sin(phase * M_PI) / 2. + 0.5; + const auto alpha = opacity * (0.2 + 0.75 * pulse); + p.setBrush(anim::with_alpha(st::mediaviewSaveMsgFg->c, alpha)); + const auto ax = arrowsX + i * arrowStep; + auto path = QPainterPath(); + path.moveTo(ax + arrowRound, cy - innerHalfH); + path.lineTo(ax + arrowRound + innerArrowWidth, cy); + path.lineTo(ax + arrowRound, cy + innerHalfH); + path.closeSubpath(); + if (arrowRound > 0.) { + path = path.united(stroker.createStroke(path)); + } + p.drawPath(path); + } + p.restore(); + p.setOpacity(1); +} + +bool OverlayWidget::isSpeedBoostShown() const { + return _speedBoostActive || _speedBoostAnimation.animating(); +} + +void OverlayWidget::updateSpeedBoost() { + update(_speedBoostRect); +} + +bool OverlayWidget::saveControlLocked() const { + const auto story = _stories ? _stories->story() : nullptr; + return story + && story->canDownloadIfPremium() + && !story->canDownloadChecked(); +} + +void OverlayWidget::paintControls( + not_null renderer, + float64 opacity) { + struct Control { + Over state = Over::None; + bool visible = false; + const QRect &over; + const QRect &inner; + const style::icon &icon; + bool nonbright = false; + }; + // When adding / removing controls please update RendererGL. + const Control controls[] = { + { + Over::Left, + _leftNavVisible, + _leftNavOver, + _leftNavIcon, + _stories ? st::storiesLeft : st::mediaviewLeft, + true }, + { + Over::Right, + _rightNavVisible, + _rightNavOver, + _rightNavIcon, + _stories ? st::storiesRight : st::mediaviewRight, + true }, + { + Over::Save, + _saveVisible, + _saveNavOver, + _saveNavIcon, + (saveControlLocked() + ? st::mediaviewSaveLocked + : st::mediaviewSave) }, + { + Over::Share, + _shareVisible, + _shareNavOver, + _shareNavIcon, + st::mediaviewShare }, + { + Over::Rotate, + _rotateVisible, + _rotateNavOver, + _rotateNavIcon, + st::mediaviewRotate }, + { + Over::More, + true, + _moreNavOver, + _moreNavIcon, + st::mediaviewMore }, + { + Over::Draw, + _drawVisible, + _drawNavOver, + _drawNavIcon, + st::mediaviewDraw }, + { + Over::Recognize, + _recognizeVisible, + _recognizeNavOver, + _recognizeNavIcon, + st::mediaviewRecognize }, + }; + + renderer->paintControlsStart(); + for (const auto &control : controls) { + if (!control.visible) { + continue; + } + const auto progress = overLevel(control.state); + const auto bg = progress; + const auto icon = controlOpacity(progress, control.nonbright); + renderer->paintControl( + control.state, + control.over, + bg * opacity, + control.inner, + icon * opacity, + control.icon); + } +} + +float64 OverlayWidget::controlOpacity( + float64 progress, + bool nonbright) const { + if (nonbright && _stories) { + return progress * kStoriesNavOverOpacity + + (1. - progress) * kStoriesNavOpacity; + } + const auto normal = _windowed + ? kNormalIconOpacity + : kMaximizedIconOpacity; + return progress + (1. - progress) * normal; +} + +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(Over::Header) : 0; + p.setOpacity(controlOpacity(o) * 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(Over::Name) : 0.; + p.setOpacity(controlOpacity(o) * 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); + } + } + + // separator + if (_separatorNav.isValid()) { + const auto separator = _separatorNav.translated(shift); + if (separator.intersects(clip)) { + p.setOpacity(controlOpacity(0.) * opacity); + p.drawText( + separator.left(), + separator.top() + st::mediaviewFont->ascent, + Ui::kQBullet); + } + } + + // date + if (date.intersects(clip)) { + float64 o = overLevel(Over::Date); + p.setOpacity(controlOpacity(o) * 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(_separatorNav).united(_dateNav); +} + +void OverlayWidget::paintCaptionContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity) { + const auto full = outer.marginsRemoved(st::mediaviewCaptionPadding); + const auto inner = full.marginsRemoved( + _stories ? _stories->repostCaptionPadding() : QMargins()); + if (_stories) { + p.setOpacity(1.); + if (_stories->repost()) { + _stories->drawRepostInfo(p, full.x(), full.y(), full.width()); + } + } else { + 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); + RefreshCaptionQuoteCaches(_captionBlockquoteCache, _captionPreCache); + _caption.draw(p, { + .position = inner.topLeft(), + .availableWidth = inner.width(), + .palette = &st::mediaviewTextPalette, + .pre = &_captionPreCache, + .blockquote = &_captionBlockquoteCache, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .pausedEmoji = On(PowerSaving::kEmojiChat), + .pausedSpoiler = On(PowerSaving::kChatSpoiler), + .elisionHeight = inner.height(), + .elisionRemoveFromEnd = _captionSkipBlockWidth, + }); + + if (_captionShowMoreWidth > 0) { + const auto padding = st::storiesShowMorePadding; + const auto showMoreLeft = outer.x() + + outer.width() + - padding.right() + - _captionShowMoreWidth; + const auto showMoreTop = outer.y() + + outer.height() + - padding.bottom() + - st::storiesShowMoreFont->height; + const auto underline = _captionExpandLink + && ClickHandler::showAsActive(_captionExpandLink); + p.setFont(underline + ? st::storiesShowMoreFont->underline() + : st::storiesShowMoreFont); + p.drawTextLeft( + showMoreLeft, + showMoreTop, + width(), + tr::lng_stories_show_more(tr::now)); + } + } +} + +QRect OverlayWidget::captionGeometry() const { + return _captionRect.marginsAdded( + st::mediaviewCaptionPadding + ).marginsAdded( + _stories ? _stories->repostCaptionPadding() : QMargins()); +} + +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) { + if (_processingKeyPress) { + return; + } + _processingKeyPress = true; + const auto guard = gsl::finally([&] { _processingKeyPress = false; }); + const auto key = LayoutIndependentKey(e); + const auto modifiers = e->modifiers(); + const auto ctrl = modifiers.testFlag(Qt::ControlModifier); + if (_stories) { + if (key == Qt::Key_Space && _down != Over::Video) { + _stories->togglePaused(!_stories->paused()); + return; + } + } else 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_K) { + playbackPauseResume(); + return; + } else if (key == Qt::Key_Space) { + if (!e->isAutoRepeat()) { + _speedBoostHoldTimer.callOnce( + st::mediaviewSpeedBoostHoldDelay); + } + return; + } else if (key == Qt::Key_J) { + activateControls(); + seekRelativeTime(-kSeekTimeMsLong); + return; + } else if (key == Qt::Key_L) { + activateControls(); + seekRelativeTime(kSeekTimeMsLong); + return; + } else if ((key == Qt::Key_Period || key == Qt::Key_Comma) + && _streamed->instance.player().paused()) { + activateControls(); + _frameStepPending += (key == Qt::Key_Period) ? 1 : -1; + if (!_frameStepThrottle.isActive()) { + flushPendingFrameStep(); + _frameStepThrottle.callOnce(kFrameStepThrottleMs); + } + return; + } else if (modifiers.testFlag(Qt::AltModifier) + && (key == Qt::Key_Left || key == Qt::Key_Right) + && _streamed->controls + && _streamed->controls->hasTimestamps()) { + const auto &state = _streamed->instance.info().video.state; + const auto duration = state.duration; + if (duration > 0) { + const auto progress = state.position + / float64(duration); + const auto &controls = _streamed->controls; + if (key == Qt::Key_Right) { + if (const auto ts = controls->nextTimestamp(progress)) { + activateControls(); + restartAtProgress(ts->position); + showChapterIndicator(ts->label, 1); + } + } else { + if (const auto ts = controls->prevTimestamp(progress)) { + activateControls(); + restartAtProgress(ts->position); + showChapterIndicator(ts->label, -1); + } else { + activateControls(); + restartAtSeekPosition(0); + } + } + } + return; + } else if (_fullScreenVideo) { + if (key == Qt::Key_Escape) { + playbackToggleFullScreen(); + } else if (ctrl) { + } 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(); + } + // Ctrl + S is claimed against other shortcuts in eventFilter(). + } else if (e == QKeySequence::Save || e == QKeySequence::SaveAs) { + saveAs(); + } else if (key == Qt::Key_Copy || (key == Qt::Key_C && ctrl)) { + if (!copyRecognitionSelection()) { + 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_H && !_stories) { + if (_flip & Qt::Horizontal) { + _flip &= ~Qt::Horizontal; + } else { + _flip |= Qt::Horizontal; + } + if (_photo) { + validatePhotoCurrentImage(); + redisplayContent(); + } + } else if (key == Qt::Key_V && !_stories) { + if (_flip & Qt::Vertical) { + _flip &= ~Qt::Vertical; + } else { + _flip |= Qt::Vertical; + } + if (_photo) { + validatePhotoCurrentImage(); + redisplayContent(); + } + } else if (key == Qt::Key_Right) { + if (_controlsHideTimer.isActive()) { + activateControls(); + } + if (!moveToNext(1) && _stories) { + storiesClose(); + } + } 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 (_stories) { + _stories->tryProcessKeyInput(e); + } +} + +void OverlayWidget::handleKeyRelease(not_null e) { + if (e->isAutoRepeat()) { + return; + } + if (e->key() == Qt::Key_Space && _streamed && !_stories) { + if (_speedBoostHoldTimer.isActive()) { + _speedBoostHoldTimer.cancel(); + playbackPauseResume(); + } else if (_speedBoostActive) { + stopSpeedBoost(); + } + } +} + +void OverlayWidget::handleWheelEvent(not_null e) { + constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep); + + const auto angle = e->angleDelta(); + if (_swipeNavigating || std::abs(angle.x()) > std::abs(angle.y())) { + _verticalWheelDelta = 0; + return; + } + const auto acceptForJump = !_stories + && ((e->source() == Qt::MouseEventNotSynthesized) + || (e->source() == Qt::MouseEventSynthesizedBySystem)); + const auto anchor = zoomAnchor(e->globalPosition()); + _verticalWheelDelta += angle.y(); + while (std::abs(_verticalWheelDelta) >= step) { + if (_verticalWheelDelta < 0) { + _verticalWheelDelta += step; + if (e->modifiers().testFlag(Qt::ControlModifier)) { + zoomOut(anchor); + } else if (acceptForJump) { + moveToNext(1); + } + } else { + _verticalWheelDelta -= step; + if (e->modifiers().testFlag(Qt::ControlModifier)) { + zoomIn(anchor); + } else if (acceptForJump) { + moveToNext(-1); + } + } + } +} + +void OverlayWidget::setupSwipeNavigation() { + struct State { + Ui::Controls::SwipeBackResult back; + bool mirrored = false; + }; + const auto state = lifetime().make_state(); + const auto colors = []() -> std::pair { + return { + st::mediaviewControlBg->c, + st::mediaviewControlFg->c, + }; + }; + auto update = [=](Ui::Controls::SwipeContextData data) { + _swipeNavigating = (data.translation != 0); + if (data.translation != 0) { + const auto mirrored = (data.translation < 0); + if (!state->back.callback || state->mirrored != mirrored) { + state->back = Ui::Controls::SetupSwipeBack( + _body, + colors, + mirrored, + false, + [=] { return _minUsedTop + _maxUsedHeight / 2; }); + state->mirrored = mirrored; + } + state->back.callback(data); + } else if (state->back.lifetime) { + state->back = {}; + } + }; + auto init = [=](Ui::Controls::SwipeHandlerInitData data) { + if (_stories) { + return Ui::Controls::SwipeHandlerFinishData(); + } + const auto next = (data.direction == Qt::LeftToRight); + if (next ? !_rightNavVisible : !_leftNavVisible) { + return Ui::Controls::SwipeHandlerFinishData(); + } + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + moveToNext(next ? 1 : -1); + }); + }; + Ui::Controls::SetupSwipeHandler({ + .widget = _body, + .update = std::move(update), + .init = std::move(init), + }); +} + +bool OverlayWidget::handleNativeGesture(not_null e) { + if (e->gestureType() == Qt::BeginNativeGesture) { + _pinchZoomAccumulated = 0.; + _zoomAtLimit = false; + return false; + } else if (e->gestureType() != Qt::ZoomNativeGesture) { + return false; + } else if (_stories || _fullScreenVideo) { + return false; + } + const auto stepZoom = [&](auto &&zoom) { + const auto before = _zoom; + zoom(); + if (_zoom != before) { + _zoomAtLimit = false; + } else if (!_zoomAtLimit) { + _zoomAtLimit = true; + base::Platform::Haptic(); + } + }; + const auto anchor = zoomAnchor(e->globalPos()); + _pinchZoomAccumulated += e->value(); + while (_pinchZoomAccumulated >= kPinchZoomStep) { + _pinchZoomAccumulated -= kPinchZoomStep; + stepZoom([&] { zoomIn(anchor); }); + } + while (_pinchZoomAccumulated <= -kPinchZoomStep) { + _pinchZoomAccumulated += kPinchZoomStep; + stepZoom([&] { zoomOut(anchor); }); + } + return true; +} + +QPoint OverlayWidget::zoomAnchor(QPointF globalPosition) const { + return _widget->mapFromGlobal(globalPosition.toPoint()); +} + +void OverlayWidget::setZoomLevel( + int newZoom, + bool force, + std::optional anchor) { + if (_stories + || (!force && _zoom == newZoom) + || (_fullScreenVideo && newZoom != kZoomToScreenLevel)) { + 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); + const auto anchorX = anchor ? float64(anchor->x()) : (width() / 2.); + const auto anchorY = anchor + ? float64(anchor->y()) + : (_availableHeight / 2.); + _oldGeometry = contentGeometry(); + _geometryAnimation.stop(); + + _w = contentSize.width(); + _h = contentSize.height(); + if (z >= 0) { + nx = (_x - anchorX) / (z + 1); + ny = (_y - anchorY) / (z + 1); + } else { + nx = (_x - anchorX) * (-z + 1); + ny = (_y - anchorY) * (-z + 1); + } + _zoom = newZoom; + z = (_zoom == kZoomToScreenLevel) ? full : _zoom; + if (z > 0) { + _w = int(base::SafeRound(_w * (z + 1))); + _h = int(base::SafeRound(_h * (z + 1))); + _x = int(base::SafeRound(nx * (z + 1) + anchorX)); + _y = int(base::SafeRound(ny * (z + 1) + anchorY)); + } else { + _w = int(base::SafeRound(_w / (-z + 1))); + _h = int(base::SafeRound(_h / (-z + 1))); + _x = int(base::SafeRound(nx / (-z + 1) + anchorX)); + _y = int(base::SafeRound(ny / (-z + 1) + anchorY)); + } + 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::entityForInstantViewMedia(int index) const { + Expects(_instantViewMediaData.has_value()); + Expects(_session != nullptr); + + const auto &items = *_instantViewMediaData; + if (!_message || index < 0 || index >= items.size()) { + return { v::null, nullptr }; + } + if (const auto document = std::get_if(&items[index])) { + return { *document, _message, _topicRootId, _monoforumPeerId }; + } else if (const auto photo = std::get_if(&items[index])) { + return { *photo, _message, _topicRootId, _monoforumPeerId }; + } + 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, _monoforumPeerId }; + } else if (const auto photo = std::get_if(&items[index])) { + return { *photo, _message, _topicRootId, _monoforumPeerId }; + } + 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, _monoforumPeerId }; + } else if (const auto document = media->document()) { + return { document, item, _topicRootId, _monoforumPeerId }; + } + } + return { v::null, item, _topicRootId, _monoforumPeerId }; + } + return { v::null, nullptr }; +} + +OverlayWidget::Entity OverlayWidget::entityByIndex(int index) const { + if (_instantViewMediaData) { + return entityForInstantViewMedia(index); + } else 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, + StoriesContext> context) { + if (const auto item = std::get_if(&context)) { + if (_message != item->item) { + checkSingleViewMediaBurn(); + } + _message = item->item; + _history = _message->history(); + _peer = _history->peer; + markTimedMediaRead(); + _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); + _monoforumPeerId = _peer->amMonoforumAdmin() + ? item->monoforumPeerId + : PeerId(); + setStoriesPeer(nullptr); + } else if (const auto peer = std::get_if>(&context)) { + _peer = *peer; + _history = _peer->owner().history(_peer); + _message = nullptr; + _topicRootId = MsgId(); + _monoforumPeerId = PeerId(); + setStoriesPeer(nullptr); + } else if (const auto story = std::get_if(&context)) { + _message = nullptr; + _topicRootId = MsgId(); + _monoforumPeerId = PeerId(); + _history = nullptr; + _peer = nullptr; + setStoriesPeer(story->peer); + const auto &stories = story->peer->owner().stories(); + const auto maybeStory = stories.lookup( + { story->peer->id, story->id }); + if (maybeStory) { + _stories->show(*maybeStory, story->within); + _dropdown->raise(); + } + } else { + _message = nullptr; + _topicRootId = MsgId(); + _monoforumPeerId = PeerId(); + _history = nullptr; + _peer = nullptr; + setStoriesPeer(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; + refreshScreenshotProtection(); +} + +void OverlayWidget::setStoriesPeer(PeerData *peer) { + const auto session = peer ? &peer->session() : nullptr; + if (!session && !_storiesSession) { + Assert(!_stories); + Assert(!_videoStream); + } else if (!peer) { + _videoStream = nullptr; + _stories = nullptr; + _storiesSession = nullptr; + _storiesChanged.fire({}); + updateNavigationControlsGeometry(); + } else if (_storiesSession != session) { + _videoStream = nullptr; + _stories = nullptr; + _storiesSession = session; + const auto delegate = static_cast(this); + _stories = std::make_unique(delegate); + _stories->finalShownGeometryValue( + ) | rpl::skip(1) | rpl::on_next([=] { + updateControlsGeometry(); + }, _stories->lifetime()); + _storiesChanged.fire({}); + } +} + +void OverlayWidget::setSession(not_null session) { + if (_session == session) { + return; + } + + clearSession(); + _session = session; + _window->setWindowIcon(Window::CreateIcon(session)); + + session->downloaderTaskFinished( + ) | rpl::on_next([=] { + if (!isHidden()) { + updateControls(); + checkForSaveLoaded(); + } + }, _sessionLifetime); + + session->data().documentLoadProgress( + ) | rpl::filter([=] { + return !isHidden(); + }) | rpl::on_next([=](not_null document) { + documentUpdated(document); + }, _sessionLifetime); + + session->data().itemIdChanged( + ) | rpl::on_next([=](const Data::Session::IdChange &change) { + changingMsgId(change.newId, change.oldId); + }, _sessionLifetime); + + session->data().itemRemoved( + ) | rpl::filter([=](not_null item) { + return (_message == item); + }) | rpl::on_next([=] { + const auto media = _message->media(); + if (media + && media->ttlSecondsSingleView() + && !isHidden() + && (_photo || _document)) { + setContext(v::null); + updateControls(); + } else { + close(); + clearSession(); + } + }, _sessionLifetime); + + session->data().itemViewRefreshRequest( + ) | rpl::filter([=](not_null item) { + return (_message == item) && !isHidden(); + }) | rpl::on_next([=] { + const auto media = _message->media(); + const auto same = media + && ((_photo && media->photo() == _photo) + || (_document && media->document() == _document)); + if (same) { + return; + } + const auto playing = _streamed + && _document + && !_streamed->instance.player().finished(); + if (playing) { + _ttlDeferredClose = true; + } else { + close(); + } + }, _sessionLifetime); + + session->account().sessionChanges( + ) | rpl::on_next([=] { + clearSession(); + }, _sessionLifetime); +} + +bool OverlayWidget::moveToNext(int delta) { + if (_stories) { + return _stories->subjumpFor(delta); + } else 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, + entity.monoforumPeerId, + }); + } 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()) { + (*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 = Over::None; + if (!ClickHandler::getPressed()) { + if ((_over == Over::Left && moveToNext(-1)) + || (_over == Over::Right && moveToNext(1)) + || (_stories + && _over == Over::LeftStories + && _stories->jumpFor(-1)) + || (_stories + && _over == Over::RightStories + && _stories->jumpFor(1))) { + _lastAction = position; + } else if (_over == Over::Name + || _over == Over::Date + || _over == Over::Header + || _over == Over::Save + || _over == Over::Share + || _over == Over::Rotate + || _over == Over::Draw + || _over == Over::Recognize + || _over == Over::Icon + || _over == Over::More + || _over == Over::Video) { + _down = _over; + if (_over == Over::Video && _stories) { + _stories->contentPressed(true); + } else if (_over == Over::Video + && _streamed + && !_stories) { + _speedBoostFromMouse = true; + _speedBoostDragAccum = position.x(); + _speedBoostHoldTimer.callOnce( + st::mediaviewSpeedBoostHoldDelay); + } + } else if (const auto at = ((_showRecognitionResults + && _recognitionResult.success + && !_recognitionResult.items.empty()) + ? _recognition.positionAt( + position, + finalContentRect(), + _rotation, + false) + : RecognitionPosition()) + ; at.item >= 0) { + _recognition.start(at); + _mStart = position; + update(); + } 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 != Over::Video || button != Qt::LeftButton) { + return false; + } + _speedBoostHoldTimer.cancel(); + _speedBoostFromMouse = false; + if (_speedBoostActive) { + stopSpeedBoost(); + } + if (_stories) { + if (ClickHandler::getActive()) { + return false; + } + toggleFullScreen(_windowed); + } else if (!_streamed) { + return false; + } else { + playbackToggleFullScreen(); + playbackPauseResume(); + } + return true; +} + +void OverlayWidget::snapXY() { + auto xmin = width() - _w, xmax = 0; + auto ymin = height() - _h, ymax = _minUsedTop; + accumulate_min(xmin, (width() - _w) / 2); + accumulate_max(xmax, (width() - _w) / 2); + const auto centerY = _fullScreenVideo + ? (_minUsedTop + (_maxUsedHeight - _h) / 2) + : (_skipTop + (_availableHeight - _h) / 2); + accumulate_min(ymin, centerY); + accumulate_max(ymax, centerY); + accumulate_max(_x, xmin); + accumulate_min(_x, xmax); + accumulate_max(_y, ymin); + accumulate_min(_y, ymax); +} + +auto OverlayWidget::scaledRecognitionRect(QPoint position) +const -> std::optional { + auto contentRect = finalContentRect(); + if (_rotation) { + auto transform = QTransform(); + const auto center = rect::center(contentRect); + transform.translate(center.x(), center.y()); + transform.rotate(-_rotation); + transform.translate(-center.x(), -center.y()); + contentRect = transform.mapRect(contentRect); + position = transform.map(position); + } + const auto imageSize = _staticContent.size() + / style::DevicePixelRatio(); + if (imageSize.isEmpty() || !contentRect.contains(position)) { + return std::nullopt; + } + const auto scale = contentRect.width() / float64(imageSize.width()); + for (const auto &item : _recognitionResult.items) { + const auto &rect = item.rect; + const auto scaledRect = QRect( + contentRect.x() + int(rect.x() * scale), + contentRect.y() + int(rect.y() * scale), + int(rect.width() * scale), + int(rect.height() * scale)); + if (scaledRect.contains(position)) { + return Platform::TextRecognition::RectWithText{ + .text = item.text, + .rect = scaledRect, + }; + } + } + return std::nullopt; +} + +bool OverlayWidget::recognitionTakesMouse(QPoint position) const { + return _showRecognitionResults + && _recognitionResult.success + && !_recognitionResult.items.empty() + && (_recognition.selecting() + || scaledRecognitionRect(position).has_value()); +} + +void OverlayWidget::updateRecognitionSelection(QPoint position) { + const auto focus = _recognition.positionAt( + position, + finalContentRect(), + _rotation, + true); + if (_recognition.updateFocus(focus)) { + update(); + } +} + +void OverlayWidget::clearRecognitionSelection() { + if (_recognition.clear()) { + update(); + } +} + +bool OverlayWidget::copyRecognitionSelection() { + const auto text = _recognition.selectedText(); + if (text.isEmpty()) { + return false; + } + TextUtilities::SetClipboardText(TextForMimeData::Simple(text)); + showSaveMsgToastWith( + QString(), + { tr::lng_text_copied(tr::now) }, + 1000); + return true; +} + +void OverlayWidget::handleMouseMove(QPoint position) { + if (_speedBoostFromMouse && !_speedBoostActive) { + if (_speedBoostHoldTimer.isActive()) { + _speedBoostDragAccum = position.x(); + } + } else if (_speedBoostActive && _speedBoostFromMouse) { + const auto delta = position.x() - _speedBoostDragAccum; + _speedBoostDragAccum = position.x(); + const auto perUnit = float64(st::mediaviewSpeedBoostDragPerUnit); + if (perUnit > 0.) { + const auto speed = std::clamp( + _speedBoostSpeed + delta / perUnit, + kSpeedMin, + kSpeedMax); + if (_speedBoostSpeed != speed) { + _speedBoostSpeed = speed; + if (_streamed) { + _streamed->instance.setSpeed(speed); + } + updateSpeedBoost(); + } + } + } + updateOver(position); + if (_lastAction.x() >= 0 + && ((position - _lastAction).manhattanLength() + >= st::mediaviewDeltaFromLastAction)) { + _lastAction = QPoint(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction); + } + if (_recognition.selecting()) { + if (!_recognition.dragged() + && ((position - _mStart).manhattanLength() + >= QApplication::startDragDistance())) { + _recognition.setDragged(true); + } + updateRecognitionSelection(position); + setCursor(style::cur_text); + return; + } + if (_recognitionResult.success + && !_recognitionResult.items.empty() + && _showRecognitionResults) { + if (scaledRecognitionRect(position)) { + setCursor(style::cur_text); + } else if (!_pressed) { + updateCursor(); + } + } + 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 > _maxUsedHeight) { + 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(Over state) { + using Type = Stories::SiblingType; + switch (state) { + case Over::Left: + update(_stories ? _leftNavIcon : _leftNavOver); + break; + case Over::Right: + update(_stories ? _rightNavIcon : _rightNavOver); + break; + case Over::LeftStories: + update(_stories + ? _stories->sibling(Type::Left).layout.geometry + : QRect()); + break; + case Over::RightStories: + update(_stories + ? _stories->sibling(Type::Right).layout.geometry + : QRect()); + break; + case Over::Name: update(_nameNav); break; + case Over::Date: update(_dateNav); break; + case Over::Save: update(_saveNavOver); break; + case Over::Share: update(_shareNavOver); break; + case Over::Rotate: update(_rotateNavOver); break; + case Over::Draw: update(_drawNavOver); break; + case Over::Icon: update(_docIconRect); break; + case Over::Header: update(_headerNav); break; + case Over::More: update(_moreNavOver); break; + case Over::Recognize: update(_recognizeNavOver); break; + } +} + +bool OverlayWidget::updateOverState(Over newState) { + bool result = true; + if (_over != newState) { + if (!_stories && newState == Over::More && !_ignoringDropdown) { + _dropdownShowTimer.callOnce(0); + } else { + _dropdownShowTimer.cancel(); + } + updateOverRect(_over); + updateOverRect(newState); + if (_over != Over::None) { + _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 != Over::None) { + _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) && !_fullScreenVideo) { + auto request = Ui::Text::StateRequestElided(); + const auto lineHeight = st::mediaviewCaptionStyle.font->height; + request.lines = _captionRect.height() / lineHeight; + request.removeFromEnd = _captionSkipBlockWidth; + auto textState = _caption.getStateElided(pos - _captionRect.topLeft(), _captionRect.width(), request); + lnk = textState.link; + if (_stories && !lnk) { + lnk = ensureCaptionExpandLink(); + } + lnkhost = this; + } else if (_stories && captionGeometry().contains(pos)) { + const auto padding = st::mediaviewCaptionPadding; + const auto handler = _stories->lookupRepostHandler( + pos - captionGeometry().marginsRemoved(padding).topLeft()); + if (handler) { + lnk = handler.link; + lnkhost = handler.host; + setCursor(style::cur_pointer); + _cursorOverriden = true; + } + } else if (_groupThumbs && _groupThumbsRect.contains(pos)) { + const auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop); + lnk = _groupThumbs->getState(point); + lnkhost = this; + } else if (_stories) { + lnk = _stories->lookupAreaHandler(pos); + lnkhost = this; + } + + // retina + if (pos.x() == width()) { + pos.setX(pos.x() - 1); + } + if (pos.y() == height()) { + pos.setY(pos.y() - 1); + } + + if (_cursorOverriden && (!lnkhost || lnkhost == this)) { + _cursorOverriden = false; + setCursor(style::cur_default); + } + ClickHandler::setActive(lnk, lnkhost); + + if (_pressed || _dragging) return; + + using SiblingType = Stories::SiblingType; + if (_fullScreenVideo) { + updateOverState(Over::Video); + } else if (_leftNavVisible + && _leftNav.contains(pos) + && !recognitionTakesMouse(pos)) { + updateOverState(Over::Left); + } else if (_rightNavVisible + && _rightNav.contains(pos) + && !recognitionTakesMouse(pos)) { + updateOverState(Over::Right); + } else if (_stories + && _stories->sibling( + SiblingType::Left).layout.geometry.contains(pos)) { + updateOverState(Over::LeftStories); + } else if (_stories + && _stories->sibling( + SiblingType::Right).layout.geometry.contains(pos)) { + updateOverState(Over::RightStories); + } else if (!_stories && _from && _nameNav.contains(pos)) { + updateOverState(Over::Name); + } else if (!_stories + && _message + && _message->isRegular() + && _dateNav.contains(pos)) { + updateOverState(Over::Date); + } else if (!_stories && _headerHasLink && _headerNav.contains(pos)) { + updateOverState(Over::Header); + } else if (_saveVisible && _saveNav.contains(pos)) { + updateOverState(Over::Save); + } else if (_shareVisible && _shareNav.contains(pos)) { + updateOverState(Over::Share); + } else if (_rotateVisible && _rotateNav.contains(pos)) { + updateOverState(Over::Rotate); + } else if (_document + && documentBubbleShown() + && _docIconRect.contains(pos)) { + updateOverState(Over::Icon); + } else if (_moreNav.contains(pos)) { + updateOverState(Over::More); + } else if (_drawVisible && _drawNav.contains(pos)) { + updateOverState(Over::Draw); + } else if (_recognizeVisible && _recognizeNav.contains(pos)) { + updateOverState(Over::Recognize); + } else if (contentShown() && finalContentRect().contains(pos)) { + if (_stories) { + updateOverState(Over::Video); + } else if (_streamed + && _document + && (_document->isVideoFile() || _document->isVideoMessage())) { + updateOverState(Over::Video); + } else if (!_streamed && _document && !_documentMedia->loaded()) { + updateOverState(Over::Icon); + } else if (_over != Over::None) { + updateOverState(Over::None); + } + } else if (_over != Over::None) { + updateOverState(Over::None); + } +} + +ClickHandlerPtr OverlayWidget::ensureCaptionExpandLink() { + if (!_captionExpandLink) { + const auto toggle = crl::guard(_widget, [=] { + if (_stories) { + _stories->showFullCaption(); + } + }); + _captionExpandLink = std::make_shared(toggle); + } + return _captionExpandLink; +} + +void OverlayWidget::handleMouseRelease( + QPoint position, + Qt::MouseButton button) { + updateOver(position); + + if (const auto activated = ClickHandler::unpressed()) { + if (activated->url() == 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()), + .show = _stories ? _stories->uiShow() : uiShow(), + .dark = true, + }) + }); + return; + } + + if (_recognition.selecting()) { + _recognition.setSelecting(false); + if (_recognition.dragged()) { + _recognition.setDragged(false); + _over = _down = Over::None; + _pressed = false; + _dragging = 0; + return; + } + _recognition.setDragged(false); + clearRecognitionSelection(); + } + + if (_recognitionResult.success + && !_dragging + && !_recognitionResult.items.empty() + && _showRecognitionResults + && button == Qt::LeftButton) { + if (const auto result = scaledRecognitionRect(position)) { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(result->text)); + showSaveMsgToastWith( + QString(), + { tr::lng_text_copied(tr::now) }, + 1000); + _over = _down = Over::None; + _pressed = false; + _dragging = 0; + return; + } + } + + if (_over == Over::Name && _down == Over::Name) { + if (_from) { + if (!_windowed) { + close(); + } + if (const auto window = findWindow(true)) { + window->showPeerInfo(_from); + window->window().activate(); + } + } + } else if (_over == Over::Date && _down == Over::Date) { + toMessage(); + } else if (_over == Over::Header && _down == Over::Header) { + showMediaOverview(); + } else if (_over == Over::Save && _down == Over::Save) { + downloadMedia(); + } else if (_over == Over::Share && _down == Over::Share && _stories) { + _stories->shareRequested(); + } else if (_over == Over::Rotate && _down == Over::Rotate) { + playbackControlsRotate(); + } else if (_over == Over::Icon && _down == Over::Icon) { + handleDocumentClick(); + } else if (_over == Over::More && _down == Over::More) { + InvokeQueued(_widget, [=] { showDropdown(); }); + } else if (_over == Over::Draw && _down == Over::Draw) { + draw(); + } else if (_over == Over::Recognize && _down == Over::Recognize) { + recognize(); + } else if (_down == Over::Video) { + if (_stories) { + _stories->contentPressed(false); + } else { + _speedBoostHoldTimer.cancel(); + if (_speedBoostActive && _speedBoostFromMouse) { + stopSpeedBoost(); + } else { + _speedBoostFromMouse = false; + if (_over == Over::Video + && _streamed + && !_window->mousePressCancelled()) { + if (_sponsoredButton && _session && _message) { + const auto sponsoredMessages + = &_session->sponsoredMessages(); + const auto fullId = _message->fullId(); + const auto details = sponsoredMessages->lookupDetails( + fullId); + if (const auto link = details.link; !link.isEmpty()) { + UrlClickHandler::Open(link); + sponsoredMessages->clicked(fullId, true, true); + hide(); + } + } else { + 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 (!_windowed + && position.y() > st::mediaviewTitleButton.height + && (position - _lastAction).manhattanLength() + >= st::mediaviewDeltaFromLastAction) { + if (_recognition.hasSelection()) { + clearRecognitionSelection(); + } else if (_themePreviewShown) { + if (!_themePreviewRect.contains(position)) { + close(); + } + } else if (!_document + || documentContentShown() + || !documentBubbleShown() + || !_docRect.contains(position)) { + if (!_stories || _stories->closeByClickAt(position)) { + close(); + } + } + } + _pressed = false; + } + _down = Over::None; + if (!isHidden()) { + activateControls(); + } +} + +bool OverlayWidget::handleContextMenu(std::optional position) { + if (_layerBg->topShownLayer()) { + return false; + } else if (position) { + if (!QRect(_x, _y, _w, _h).contains(*position) + || position->y() <= st::mediaviewTitleButton.height) { + return false; + } + } + _menu = base::make_unique_q( + _window, + st::mediaviewPopupMenu); + fillContextMenuActions(Ui::Menu::CreateAddActionCallback(_menu)); + + if (_menu->empty()) { + _menu = nullptr; + return true; + } + if (_stories) { + _stories->menuShown(true); + } + _menu->setDestroyedCallback(crl::guard(_widget, [=] { + if (_stories) { + _stories->menuShown(false); + } + activateControls(); + _receiveMouse = false; + InvokeQueued(_widget, [=] { receiveMouse(); }); + })); + + using HistoryView::Reactions::AttachSelectorResult; + const auto attached = _stories + ? _stories->attachReactionsToMenu(_menu.get(), QCursor::pos()) + : AttachSelectorResult::Skipped; + if (attached == AttachSelectorResult::Failed) { + _menu = nullptr; + return true; + } else if (attached == AttachSelectorResult::Attached) { + _menu->popupPrepared(); + } else { + _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(); + activateControls(); + } break; + + case QEvent::TouchUpdate: { + if (!_touchPress || e->touchPoints().isEmpty()) { + break; + } + if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { + _touchMove = true; + } + activateControls(); + } break; + + case QEvent::TouchEnd: { + if (!_touchPress) { + break; + } + auto weak = base::make_weak(_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; + } + e->accept(); + 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 event = static_cast(e.get()); + const auto key = LayoutIndependentKey(event); + const auto ctrl = event->modifiers().testFlag(Qt::ControlModifier); + if (key == Qt::Key_F && ctrl && _streamed) { + playbackToggleFullScreen(); + return true; + } else if (key == Qt::Key_0 && ctrl) { + zoomReset(); + return true; + } else if (event == QKeySequence::SaveAs + || event == QKeySequence::Save) { + return true; + } + return false; + } else if (type == QEvent::MouseMove + || type == QEvent::MouseButtonPress + || type == QEvent::MouseButtonRelease) { + if (object->isWidgetType() + && static_cast(object.get())->window() == _window) { + const auto mouseEvent = static_cast(e.get()); + const auto mousePosition = _body->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 (!isHidden() && !_hideWorkaround) { + _hideWorkaround = std::make_unique(_window); + const auto raw = _hideWorkaround.get(); + raw->setGeometry(_window->rect()); + raw->show(); + raw->paintRequest( + ) | rpl::on_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(); + _widget->update(); + + if (!Platform::IsMac()) { + Ui::ForceFullRepaintSync(_window); + } + _hideWorkaround = nullptr; + } +} + +Window::SessionController *OverlayWidget::findWindow(bool switchTo) const { + const auto session = _session ? _session : _storiesSession; + if (!session) { + return nullptr; + } + + const auto window = _openedFrom.get(); + if (window) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == session) { + return controller; + } + } + } + + if (switchTo) { + auto controllerPtr = (Window::SessionController*)nullptr; + const auto account = not_null(&session->account()); + const auto sessionWindow = Core::App().windowFor(account); + const auto anyWindow = (sessionWindow + && &sessionWindow->account() == account) + ? sessionWindow + : window + ? window + : sessionWindow; + 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() { + checkSingleViewMediaBurn(); + _message = nullptr; + _sharedMedia = nullptr; + _sharedMediaData = std::nullopt; + _sharedMediaDataKey = std::nullopt; + _userPhotos = nullptr; + _userPhotosData = std::nullopt; + _instantViewMedia = nullptr; + _instantViewMediaData = std::nullopt; + _collage = nullptr; + _collageData = std::nullopt; + clearStreaming(); + setStoriesPeer(nullptr); + _layerBg->hideAll(anim::type::instant); + assignMediaPointer(nullptr); + _preloadPhotos.clear(); + _preloadDocuments.clear(); + if (_menu) { + _menu->hideMenu(true); + } + _chapterText = QString(); + _chapterRect = QRect(); + _chapterAnimation.stop(); + _chapterTimer.cancel(); + _chapterArrows.clear(); + _speedBoostActive = false; + _speedBoostFromMouse = false; + _speedBoostAnimation.stop(); + _speedBoostHoldTimer.cancel(); + _speedBoostTicker.stop(); + _controlsHideTimer.cancel(); + _controlsState = ControlsShown; + _controlsOpacity = anim::value(1); + _helper->setControlsOpacity(1.); + _groupThumbs = nullptr; + _groupThumbsRect = QRect(); + _sponsoredButton = nullptr; + _voteButton.destroy(); + _pollVotersWidget.destroy(); + _pollUpdateLifetime.destroy(); + _showRecognitionResults = false; +} + +void OverlayWidget::clearAfterHide() { + _recognitionResult = {}; + _recognitionPendingSessionUniqueId = 0; + _recognitionPendingPhotoId = 0; + _recognitionPendingDocumentId = 0; + _recognitionRetryOnLarge = false; + clearRecognitionSelection(); + _ttlTooltip = nullptr; + _ttlBadge = nullptr; + _ttlBadgeItem = FullMsgId(); + _ttlBadgeDestroyAt = 0; + _ttlDeferredClose = false; + _ttlTimer.cancel(); + _body->hide(); + clearStreaming(); + destroyThemePreview(); + _radial.stop(); + _staticContent = QImage(); + _themePreview = nullptr; + _voteButton.destroyDelayed(); + _pollVotersWidget.destroyDelayed(); + _themeApply.destroyDelayed(); + _themeCancel.destroyDelayed(); + _themeShare.destroyDelayed(); +} + +void OverlayWidget::receiveMouse() { + _receiveMouse = true; +} + +void OverlayWidget::showDropdown() { + _dropdown->clearActions(); + fillContextMenuActions(Ui::Menu::CreateAddActionCallback(_dropdown)); + _dropdown->moveToRight(0, height() - _dropdown->height()); + _dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight); + _dropdown->setFocus(); + if (_stories) { + _stories->menuShown(true); + } +} + +void OverlayWidget::handleTouchTimer() { + _touchRightButton = true; +} + +void OverlayWidget::updateSaveMsg() { + update(_saveMsg); +} + +void OverlayWidget::findCurrent() { + using namespace rpl::mappers; + if (_instantViewMediaData) { + const auto key = instantViewMediaKey(); + const auto &items = *_instantViewMediaData; + const auto i = key + ? ranges::find(items, *key) + : end(items); + _index = (i != end(items)) + ? std::make_optional(int(i - begin(items))) + : std::nullopt; + _fullIndex = _index; + _fullCount = items.size(); + } else if (_sharedMediaData) { + _index = _message + ? _sharedMediaData->indexOf(_message->fullId()) + : _photo ? _sharedMediaData->indexOf(_photo) : std::nullopt; + _fullIndex = _sharedMediaData->skippedBefore() + ? (_index | func::add(*_sharedMediaData->skippedBefore())) + : std::nullopt; + _fullCount = _sharedMediaData->fullCount(); + } else if (_userPhotosData) { + _index = _photo ? _userPhotosData->indexOf(_photo->id) : std::nullopt; + _fullIndex = _userPhotosData->skippedBefore() + ? (_index | func::add(*_userPhotosData->skippedBefore())) + : std::nullopt; + _fullCount = _userPhotosData->fullCount(); + } else if (_collageData) { + const auto item = _photo ? WebPageCollage::Item(_photo) : _document; + const auto &items = _collageData->items; + const auto i = ranges::find(items, item); + _index = (i != end(items)) + ? std::make_optional(int(i - begin(items))) + : std::nullopt; + _fullIndex = _index; + _fullCount = items.size(); + } else { + _index = _fullIndex = _fullCount = std::nullopt; + } +} + +void OverlayWidget::updateHeader() { + auto index = _fullIndex ? *_fullIndex : -1; + auto count = _fullCount ? *_fullCount : -1; + if (index >= 0 && index < count && count > 1) { + if (_document) { + _headerText = tr::lng_mediaview_file_n_of_amount( + tr::now, + lt_file, + (_document->filename().isEmpty() + ? tr::lng_mediaview_doc_image(tr::now) + : _document->filename()), + lt_n, + QString::number(index + 1), + lt_amount, + QString::number(count)); + } else { + if (_user + && (index == count - 1) + && _photo + && SyncUserFallbackPhotoViewer(_user) == _photo->id) { + _headerText = tr::lng_mediaview_profile_public_photo(tr::now); + } else if (_user + && _user->hasPersonalPhoto() + && _photo + && (_photo->id == _user->userpicPhotoId())) { + _headerText = tr::lng_mediaview_profile_photo_by_you(tr::now); + } else { + _headerText = tr::lng_mediaview_n_of_amount( + tr::now, + lt_n, + QString::number(index + 1), + lt_amount, + QString::number(count)); + } + } + } else { + const auto channel = _peer ? _peer->asChannel() : nullptr; + const auto media = _message ? _message->media() : nullptr; + if (media && media->ttlSeconds()) { + _headerText = _document + ? tr::lng_mediaview_disappearing_video(tr::now) + : tr::lng_mediaview_disappearing_photo(tr::now); + } 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) { + if (_user->hasPersonalPhoto() + && _photo + && (_photo->id == _user->userpicPhotoId())) { + _headerText = tr::lng_mediaview_profile_photo_by_you(tr::now); + } else { + _headerText = tr::lng_mediaview_profile_photo(tr::now); + } + } else if (channel && channel->isCommunity()) { + _headerText = tr::lng_mediaview_community_photo(tr::now); + } else if ((_history && _history->peer->isBroadcast()) + || (channel && !channel->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(Over control) const { + auto i = _animationOpacities.find(control); + return (i == end(_animationOpacities)) + ? (_over == control ? 1. : 0.) + : i->second.current(); +} + +} // namespace View +} // namespace Media