/* 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 "calls/group/ui/desktop_capture_choose_source.h" #include "ui/widgets/rp_window.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/effects/ripple_animation.h" #include "ui/image/image.h" #include "ui/platform/ui_platform_window_title.h" #include "base/platform/base_platform_info.h" #include "webrtc/webrtc_video_track.h" #include "lang/lang_keys.h" #include "styles/style_calls.h" #include #include #include #include namespace Calls::Group::Ui::DesktopCapture { namespace { constexpr auto kColumns = 3; constexpr auto kRows = 2; struct Preview { explicit Preview(tgcalls::DesktopCaptureSource source); tgcalls::DesktopCaptureSourceHelper helper; Webrtc::VideoTrack track; rpl::lifetime lifetime; }; class SourceButton final : public RippleButton { public: using RippleButton::RippleButton; private: QImage prepareRippleMask() const override; }; QImage SourceButton::prepareRippleMask() const { return RippleAnimation::roundRectMask(size(), st::roundRadiusLarge); } class Source final { public: Source( not_null parent, tgcalls::DesktopCaptureSource source, const QString &title); void setGeometry(QRect geometry); void clearHelper(); [[nodiscard]] rpl::producer<> activations() const; void setActive(bool active); [[nodiscard]] QString deviceIdKey() const; [[nodiscard]] rpl::lifetime &lifetime(); private: void paint(); void setupPreview(); SourceButton _widget; FlatLabel _label; RoundRect _selectedRect; RoundRect _activeRect; tgcalls::DesktopCaptureSource _source; std::unique_ptr _preview; rpl::event_stream<> _activations; QImage _frame; bool _active = false; }; class ChooseSourceProcess final { public: static void Start(not_null delegate); explicit ChooseSourceProcess(not_null delegate); void activate(); private: void setupPanel(); void setupSources(); void setupGeometryWithParent(not_null parent); void fillSources(); void setupSourcesGeometry(); void updateButtonsVisibility(); void destroy(); static base::flat_map< not_null, std::unique_ptr> &Map(); const not_null _delegate; const std::unique_ptr _window; const std::unique_ptr _scroll; const not_null _inner; const not_null _bottom; const not_null _submit; const not_null _finish; const not_null _withAudio; std::vector> _sources; Source *_selected = nullptr; QString _selectedId; }; [[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() { const auto factor = style::DevicePixelRatio(); const auto size = st::desktopCaptureSourceSize * factor; return { .aspectSize = { size.width(), size.height() }, .fps = 1, .captureMouse = false, }; } Preview::Preview(tgcalls::DesktopCaptureSource source) : helper(source, SourceData()) , track(Webrtc::VideoState::Active) { helper.setOutput(track.sink()); helper.start(); } Source::Source( not_null parent, tgcalls::DesktopCaptureSource source, const QString &title) : _widget(parent, st::groupCallRipple) , _label(&_widget, title, st::desktopCaptureLabel) , _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver) , _activeRect(ImageRoundRadius::Large, st::groupCallMuted1) , _source(source) { _widget.paintRequest( ) | rpl::start_with_next([=] { paint(); }, _widget.lifetime()); _label.setAttribute(Qt::WA_TransparentForMouseEvents); _widget.sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto padding = st::desktopCapturePadding; _label.resizeToNaturalWidth( size.width() - padding.left() - padding.right()); _label.move( (size.width() - _label.width()) / 2, size.height() - _label.height() - st::desktopCaptureLabelBottom); }, _label.lifetime()); _widget.setClickedCallback([=] { setActive(true); }); } rpl::producer<> Source::activations() const { return _activations.events(); } QString Source::deviceIdKey() const { return QString::fromStdString(_source.deviceIdKey()); } void Source::setActive(bool active) { if (_active != active) { _active = active; _widget.update(); if (active) { _activations.fire({}); } } } void Source::setGeometry(QRect geometry) { _widget.setGeometry(geometry); } void Source::clearHelper() { _preview = nullptr; } void Source::paint() { auto p = QPainter(&_widget); if (_frame.isNull() && !_preview) { setupPreview(); } if (_active) { _activeRect.paint(p, _widget.rect()); } else if (_widget.isOver() || _widget.isDown()) { _selectedRect.paint(p, _widget.rect()); } _widget.paintRipple( p, { 0, 0 }, _active ? &st::shadowFg->c : nullptr); const auto size = _preview ? _preview->track.frameSize() : QSize(); const auto factor = style::DevicePixelRatio(); const auto padding = st::desktopCapturePadding; const auto rect = _widget.rect(); const auto inner = rect.marginsRemoved(padding); if (!size.isEmpty()) { const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio); const auto request = Webrtc::FrameRequest{ .resize = scaled * factor, .outer = scaled * factor, }; _frame = _preview->track.frame(request); _preview->track.markFrameShown(); } if (!_frame.isNull()) { clearHelper(); const auto size = _frame.size() / factor; const auto x = inner.x() + (inner.width() - size.width()) / 2; const auto y = inner.y() + (inner.height() - size.height()) / 2; auto hq = PainterHighQualityEnabler(p); p.drawImage(QRect(x, y, size.width(), size.height()), _frame); } } void Source::setupPreview() { _preview = std::make_unique(_source); _preview->track.renderNextFrame( ) | rpl::start_with_next([=] { if (_preview->track.frameSize().isEmpty()) { _preview->track.markFrameShown(); } _widget.update(); }, _preview->lifetime); } rpl::lifetime &Source::lifetime() { return _widget.lifetime(); } ChooseSourceProcess::ChooseSourceProcess( not_null delegate) : _delegate(delegate) , _window(std::make_unique()) , _scroll(std::make_unique(_window->body())) , _inner(_scroll->setOwnedWidget(object_ptr(_scroll.get()))) , _bottom(CreateChild(_window->body().get())) , _submit( CreateChild( _bottom.get(), tr::lng_group_call_screen_share_start(), st::desktopCaptureSubmit)) , _finish( CreateChild( _bottom.get(), tr::lng_group_call_screen_share_stop(), st::desktopCaptureFinish)) , _withAudio( CreateChild( _bottom.get(), tr::lng_group_call_screen_share_audio(tr::now), false, st::desktopCaptureWithAudio)) { setupPanel(); setupSources(); activate(); } void ChooseSourceProcess::Start(not_null delegate) { auto &map = Map(); auto i = map.find(delegate); if (i == end(map)) { i = map.emplace(delegate, nullptr).first; delegate->chooseSourceInstanceLifetime().add([=] { Map().erase(delegate); }); } if (!i->second) { i->second = std::make_unique(delegate); } else { i->second->activate(); } } void ChooseSourceProcess::activate() { if (_window->windowState() & Qt::WindowMinimized) { _window->showNormal(); } else { _window->show(); } _window->raise(); _window->activateWindow(); } [[nodiscard]] base::flat_map< not_null, std::unique_ptr> &ChooseSourceProcess::Map() { static auto result = base::flat_map< not_null, std::unique_ptr>(); return result; } void ChooseSourceProcess::setupPanel() { #ifndef Q_OS_LINUX //_window->setAttribute(Qt::WA_OpaquePaintEvent); #endif // Q_OS_LINUX //_window->setAttribute(Qt::WA_NoSystemBackground); _window->setWindowIcon(QIcon( QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); _window->setTitleStyle(st::desktopCaptureSourceTitle); const auto skips = st::desktopCaptureSourceSkips; const auto margins = st::desktopCaptureMargins; const auto padding = st::desktopCapturePadding; const auto bottomSkip = margins.right() + padding.right(); const auto bottomHeight = 2 * bottomSkip + st::desktopCaptureCancel.height; const auto width = margins.left() + kColumns * st::desktopCaptureSourceSize.width() + (kColumns - 1) * skips.width() + margins.right(); const auto height = margins.top() + kRows * st::desktopCaptureSourceSize.height() + (kRows - 1) * skips.height() + (st::desktopCaptureSourceSize.height() / 2) + bottomHeight; _window->setFixedSize({ width, height }); _window->setStaysOnTop(true); _window->body()->paintRequest( ) | rpl::start_with_next([=](QRect clip) { QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg); }, _window->lifetime()); _bottom->setGeometry(0, height - bottomHeight, width, bottomHeight); _submit->setClickedCallback([=] { if (_selectedId.isEmpty()) { return; } const auto weak = MakeWeak(_window.get()); _delegate->chooseSourceAccepted( _selectedId, !_withAudio->isHidden() && _withAudio->checked()); if (const auto strong = weak.data()) { strong->close(); } }); _finish->setClickedCallback([=] { const auto weak = MakeWeak(_window.get()); _delegate->chooseSourceStop(); if (const auto strong = weak.data()) { strong->close(); } }); const auto cancel = CreateChild( _bottom.get(), tr::lng_cancel(), st::desktopCaptureCancel); cancel->setClickedCallback([=] { _window->close(); }); rpl::combine( _submit->widthValue(), _submit->shownValue(), _finish->widthValue(), _finish->shownValue(), cancel->widthValue() ) | rpl::start_with_next([=]( int submitWidth, bool submitShown, int finishWidth, bool finishShown, int cancelWidth) { _finish->moveToRight(bottomSkip, bottomSkip); _submit->moveToRight(bottomSkip, bottomSkip); cancel->moveToRight( bottomSkip * 2 + (submitShown ? submitWidth : finishWidth), bottomSkip); }, _bottom->lifetime()); _withAudio->widthValue( ) | rpl::start_with_next([=](int width) { const auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2; _withAudio->moveToLeft(bottomSkip, top); }, _withAudio->lifetime()); _withAudio->setChecked(_delegate->chooseSourceActiveWithAudio()); _withAudio->checkedChanges( ) | rpl::start_with_next([=] { updateButtonsVisibility(); }, _withAudio->lifetime()); const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty(); _finish->setVisible(sharing); _submit->setVisible(!sharing); _window->body()->sizeValue( ) | rpl::start_with_next([=](QSize size) { _scroll->setGeometry( 0, 0, size.width(), size.height() - _bottom->height()); }, _scroll->lifetime()); _scroll->widthValue( ) | rpl::start_with_next([=](int width) { const auto rows = int(std::ceil(_sources.size() / float(kColumns))); const auto innerHeight = margins.top() + rows * st::desktopCaptureSourceSize.height() + (rows - 1) * skips.height() + margins.bottom(); _inner->resize(width, innerHeight); }, _inner->lifetime()); if (const auto parent = _delegate->chooseSourceParent()) { setupGeometryWithParent(parent); } _window->events( ) | rpl::filter([=](not_null e) { return e->type() == QEvent::Close; }) | rpl::start_with_next([=] { destroy(); }, _window->lifetime()); } void ChooseSourceProcess::setupSources() { fillSources(); setupSourcesGeometry(); } void ChooseSourceProcess::fillSources() { using Type = tgcalls::DesktopCaptureType; auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen); auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window); _withAudio->setVisible(_delegate->chooseSourceWithAudioSupported()); auto screenIndex = 0; auto windowIndex = 0; const auto active = _delegate->chooseSourceActiveDeviceId(); const auto append = [&](const tgcalls::DesktopCaptureSource &source) { const auto title = !source.isWindow() ? tr::lng_group_call_screen_title( tr::now, lt_index, QString::number(++screenIndex)) : !source.title().empty() ? QString::fromStdString(source.title()) : "Window " + QString::number(++windowIndex); const auto id = source.deviceIdKey(); _sources.push_back(std::make_unique(_inner, source, title)); const auto raw = _sources.back().get(); if (!active.isEmpty() && active.toStdString() == id) { _selected = raw; raw->setActive(true); } _sources.back()->activations( ) | rpl::filter([=] { return (_selected != raw); }) | rpl::start_with_next([=]{ if (_selected) { _selected->setActive(false); } _selected = raw; updateButtonsVisibility(); }, raw->lifetime()); }; for (const auto &source : screensManager.sources()) { append(source); } for (const auto &source : windowsManager.sources()) { append(source); } } void ChooseSourceProcess::updateButtonsVisibility() { const auto selectedId = _selected ? _selected->deviceIdKey() : QString(); if (selectedId == _delegate->chooseSourceActiveDeviceId() && (!_delegate->chooseSourceWithAudioSupported() || (_withAudio->checked() == _delegate->chooseSourceActiveWithAudio()))) { _selectedId = QString(); _finish->setVisible(true); _submit->setVisible(false); } else { _selectedId = selectedId; _finish->setVisible(false); _submit->setVisible(true); } } void ChooseSourceProcess::setupSourcesGeometry() { if (_sources.empty()) { destroy(); return; } _inner->widthValue( ) | rpl::start_with_next([=](int width) { const auto rows = int(std::ceil(_sources.size() / float(kColumns))); const auto margins = st::desktopCaptureMargins; const auto skips = st::desktopCaptureSourceSkips; const auto single = (width - margins.left() - margins.right() - (kColumns - 1) * skips.width()) / kColumns; const auto height = st::desktopCaptureSourceSize.height(); auto top = margins.top(); auto index = 0; for (auto row = 0; row != rows; ++row) { auto left = margins.left(); for (auto column = 0; column != kColumns; ++column) { _sources[index]->setGeometry({ left, top, single, height }); if (++index == _sources.size()) { break; } left += single + skips.width(); } if (index >= _sources.size()) { break; } top += height + skips.height(); } }, _inner->lifetime()); rpl::combine( _scroll->scrollTopValue(), _scroll->heightValue() ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { const auto rows = int(std::ceil(_sources.size() / float(kColumns))); const auto margins = st::desktopCaptureMargins; const auto skips = st::desktopCaptureSourceSkips; const auto height = st::desktopCaptureSourceSize.height(); auto top = margins.top(); auto index = 0; for (auto row = 0; row != rows; ++row) { const auto hidden = (top + height <= scrollTop) || (top >= scrollTop + scrollHeight); if (hidden) { for (auto column = 0; column != kColumns; ++column) { _sources[index]->clearHelper(); if (++index == _sources.size()) { break; } } } else { index += kColumns; } if (index >= _sources.size()) { break; } top += height + skips.height(); } }, _inner->lifetime()); } void ChooseSourceProcess::setupGeometryWithParent( not_null parent) { _window->createWinId(); const auto parentScreen = [&] { if (!::Platform::IsWayland()) { if (const auto screen = QGuiApplication::screenAt( parent->geometry().center())) { return screen; } } return parent->screen(); }(); const auto myScreen = _window->screen(); if (parentScreen && myScreen != parentScreen) { _window->windowHandle()->setScreen(parentScreen); } _window->move( parent->x() + (parent->width() - _window->width()) / 2, parent->y() + (parent->height() - _window->height()) / 2); } void ChooseSourceProcess::destroy() { auto &map = Map(); if (const auto i = map.find(_delegate); i != end(map)) { if (i->second.get() == this) { base::take(i->second); } } } } // namespace void ChooseSource(not_null delegate) { ChooseSourceProcess::Start(delegate); } } // namespace Calls::Group::Ui::DesktopCapture