diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ac5d5e130e..4cf23232f4 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2000,6 +2000,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_share_button" = "Share"; "lng_group_call_video" = "Video"; "lng_group_call_screen_share" = "Share"; +"lng_group_call_screen_share_start" = "Share Screen"; +"lng_group_call_screen_share_stop" = "Stop Sharing"; +"lng_group_call_screen_title" = "Screen {index}"; "lng_group_call_unmute_small" = "Unmute"; "lng_group_call_you_are_live_small" = "Mute"; "lng_group_call_force_muted_small" = "Muted"; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 6a0222abc6..e346a43ee7 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -856,17 +856,18 @@ groupCallTopBarOpen: RoundButton(groupCallTopBarJoin) { color: shadowFg; } } -groupCallBox: Box(defaultBox) { - button: RoundButton(defaultBoxButton) { - textFg: groupCallActiveFg; - textFgOver: groupCallActiveFg; - numbersTextFg: groupCallActiveFg; - numbersTextFgOver: groupCallActiveFg; - textBg: groupCallMembersBg; - textBgOver: groupCallMembersBgOver; +groupCallBoxButton: RoundButton(defaultBoxButton) { + textFg: groupCallActiveFg; + textFgOver: groupCallActiveFg; + numbersTextFg: groupCallActiveFg; + numbersTextFgOver: groupCallActiveFg; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; - ripple: groupCallRipple; - } + ripple: groupCallRipple; +} +groupCallBox: Box(defaultBox) { + button: groupCallBoxButton; margin: margins(0px, 56px, 0px, 10px); bg: groupCallMembersBg; title: FlatLabel(boxTitle) { @@ -1092,8 +1093,46 @@ groupCallStartsWhenTop: 160px; groupCallCountdownFont: font(64px semibold); groupCallCountdownTop: 52px; -desktopCaptureSourceSize: size(160px, 120px); -desktopCaptureSourceSkip: 12px; +desktopCaptureMargins: margins(12px, 8px, 12px, 6px); +desktopCaptureSourceSize: size(235px, 165px); +desktopCaptureSourceSkips: size(2px, 10px); +desktopCaptureSourceTitle: WindowTitle(groupCallTitle) { + height: 21px; +} +desktopCapturePadding: margins(7px, 7px, 7px, 33px); +desktopCaptureLabelBottom: 7px; +desktopCaptureLabel: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + maxHeight: 20px; + textFg: groupCallMembersFg; + style: semiboldTextStyle; +} +desktopCaptureCancel: RoundButton(defaultBoxButton) { + textFg: groupCallActiveFg; + textFgOver: groupCallActiveFg; + numbersTextFg: groupCallActiveFg; + numbersTextFgOver: groupCallActiveFg; + textBg: groupCallBg; + textBgOver: groupCallMembersBg; + + ripple: groupCallRipple; +} +desktopCaptureFinish: RoundButton(desktopCaptureCancel) { + textFg: groupCallMemberMutedIcon; + textFgOver: groupCallMemberMutedIcon; +} +desktopCaptureSubmit: RoundButton(desktopCaptureCancel) { + textFg: groupCallIconFg; + textFgOver: groupCallIconFg; + numbersTextFg: groupCallIconFg; + numbersTextFgOver: groupCallIconFg; + textBg: groupCallMuted1; + textBgOver: groupCallMuted1; + + ripple: RippleAnimation(groupCallRipple) { + color: groupCallMuted2; + } +} groupCallNarrowSkip: 9px; groupCallNarrowRowSkip: 8px; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index cb85e416fc..5e97a2078e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -322,7 +322,12 @@ GroupCall::~GroupCall() { } bool GroupCall::isScreenSharing() const { - return (_videoDeviceId != _videoInputId); + return (_videoDeviceId != _videoInputId) + && (_videoOutgoing->state() == Webrtc::VideoState::Active); +} + +QString GroupCall::screenSharingDeviceId() const { + return isScreenSharing() ? _videoDeviceId : QString(); } void GroupCall::toggleVideo(bool active) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index 852c6b2721..1aa4ea44ee 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -248,7 +248,8 @@ public: void setCurrentAudioDevice(bool input, const QString &deviceId); void setCurrentVideoDevice(const QString &deviceId); - bool isScreenSharing() const; + [[nodiscard]] bool isScreenSharing() const; + [[nodiscard]] QString screenSharingDeviceId() const; void toggleVideo(bool active); void switchToScreenSharing(const QString &uniqueId); diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index ce7908782f..e613850819 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1460,9 +1460,9 @@ void MembersController::updateRow( Assert(nowSsrc != 0); _soundingRowBySsrc.emplace(nowSsrc, row); } - if (isMe(row->peer())) { - row->setVideoTrack(_call->outgoingVideoTrack()); - } + //if (isMe(row->peer())) { + // row->setVideoTrack(_call->outgoingVideoTrack()); + //} } const auto nowNoSounding = _soundingRowBySsrc.empty(); if (wasNoSounding && !nowNoSounding) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 75602a10f1..b0116ae329 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -484,6 +484,10 @@ QWidget *Panel::chooseSourceParent() { return _window.get(); } +QString Panel::chooseSourceActiveDeviceId() { + return _call->screenSharingDeviceId(); +} + rpl::lifetime &Panel::chooseSourceInstanceLifetime() { return _window->lifetime(); } @@ -492,6 +496,10 @@ void Panel::chooseSourceAccepted(const QString &deviceId) { _call->switchToScreenSharing(deviceId); } +void Panel::chooseSourceStop() { + _call->toggleVideo(false); +} + void Panel::initWindow() { _window->setAttribute(Qt::WA_OpaquePaintEvent); _window->setAttribute(Qt::WA_NoSystemBackground); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index ed14ca4c37..54522397db 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -116,8 +116,10 @@ private: void subscribeToPeerChanges(); QWidget *chooseSourceParent() override; + QString chooseSourceActiveDeviceId() override; rpl::lifetime &chooseSourceInstanceLifetime() override; void chooseSourceAccepted(const QString &deviceId) override; + void chooseSourceStop() override; const not_null _call; not_null _peer; diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp index 73d4a23194..0aed00c137 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -11,8 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.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 @@ -33,6 +37,19 @@ struct Preview { 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( @@ -43,19 +60,23 @@ public: void setGeometry(QRect geometry); void clearHelper(); - [[nodiscard]] bool ready() const; - [[nodiscard]] rpl::producer<> clicks() const; + [[nodiscard]] rpl::producer<> activations() const; + void setActive(bool active); [[nodiscard]] rpl::lifetime &lifetime(); private: void paint(); void setupPreview(); - AbstractButton _widget; + SourceButton _widget; FlatLabel _label; + RoundRect _selectedRect; + RoundRect _activeRect; tgcalls::DesktopCaptureSource _source; std::unique_ptr _preview; + rpl::event_stream<> _activations; QImage _frame; + bool _active = false; }; @@ -80,11 +101,16 @@ private: std::unique_ptr> &Map(); const not_null _delegate; - const std::unique_ptr<::Ui::Window> _window; + 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; std::vector> _sources; + Source *_selected = nullptr; + QString _selectedId; }; @@ -109,25 +135,45 @@ Source::Source( not_null parent, tgcalls::DesktopCaptureSource source, const QString &title) -: _widget(parent) -, _label(&_widget, 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) { - _label.resizeToNaturalWidth(size.width()); + const auto padding = st::desktopCapturePadding; + _label.resizeToNaturalWidth( + size.width() - padding.left() - padding.right()); _label.move( (size.width() - _label.width()) / 2, - size.height() - _label.height()); + size.height() - _label.height() - st::desktopCaptureLabelBottom); }, _label.lifetime()); + + _widget.setClickedCallback([=] { + setActive(true); + }); } -rpl::producer<> Source::clicks() const { - return _widget.clicks() | rpl::to_empty; +rpl::producer<> Source::activations() const { + return _activations.events(); +} + +void Source::setActive(bool active) { + if (_active != active) { + _active = active; + _widget.update(); + if (active) { + _activations.fire({}); + } + } } void Source::setGeometry(QRect geometry) { @@ -144,14 +190,21 @@ void Source::paint() { 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::groupCallMuted2->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 = QRect( - rect.x(), - rect.y(), - rect.width(), - rect.height() - _label.height()); + const auto inner = rect.marginsRemoved(padding); if (!size.isEmpty()) { const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio); const auto request = Webrtc::FrameRequest{ @@ -190,9 +243,20 @@ rpl::lifetime &Source::lifetime() { ChooseSourceProcess::ChooseSourceProcess( not_null delegate) : _delegate(delegate) -, _window(std::make_unique<::Ui::Window>()) +, _window(std::make_unique()) , _scroll(std::make_unique(_window->body())) -, _inner(_scroll->setOwnedWidget(object_ptr(_scroll.get()))) { +, _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)) { setupPanel(); setupSources(); activate(); @@ -233,25 +297,102 @@ void ChooseSourceProcess::activate() { } void ChooseSourceProcess::setupPanel() { - const auto width = kColumns * st::desktopCaptureSourceSize.width() - + (kColumns + 1) * st::desktopCaptureSourceSkip; - const auto height = kRows * st::desktopCaptureSourceSize.height() - + (kRows + 1) * st::desktopCaptureSourceSkip - + (st::desktopCaptureSourceSize.height() / 2); + _window->setAttribute(Qt::WA_OpaquePaintEvent); + _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->setWindowFlags(Qt::WindowStaysOnTopHint); + _window->body()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(_window->body()).fillRect(clip, st::groupCallBg); + }, _window->lifetime()); + + _bottom->setGeometry(0, height - bottomHeight, width, bottomHeight); + + _submit->setClickedCallback([=] { + if (_selectedId.isEmpty()) { + return; + } + const auto weak = MakeWeak(_window.get()); + _delegate->chooseSourceAccepted(_selectedId); + 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()); + + const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty(); + _finish->setVisible(sharing); + _submit->setVisible(!sharing); + _window->body()->sizeValue( ) | rpl::start_with_next([=](QSize size) { - _scroll->setGeometry({ QPoint(), 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 height = rows * st::desktopCaptureSourceSize.height() - + (rows + 1) * st::desktopCaptureSourceSkip; - _inner->resize(width, height); + const auto innerHeight = margins.top() + + rows * st::desktopCaptureSourceSize.height() + + (rows - 1) * skips.height() + + margins.bottom(); + _inner->resize(width, std::max(height, innerHeight)); }, _inner->lifetime()); if (const auto parent = _delegate->chooseSourceParent()) { @@ -278,17 +419,42 @@ void ChooseSourceProcess::fillSources() { auto screenIndex = 0; auto windowIndex = 0; + const auto active = _delegate->chooseSourceActiveDeviceId(); const auto append = [&](const tgcalls::DesktopCaptureSource &source) { - const auto title = !source.title().empty() + 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()) - : source.isWindow() - ? "Window " + QString::number(++windowIndex) - : "Screen " + QString::number(++screenIndex); + : "Window " + QString::number(++windowIndex); + const auto id = source.deviceIdKey(); _sources.push_back(std::make_unique(_inner, source, title)); - _sources.back()->clicks( - ) | rpl::start_with_next([=, id = source.deviceIdKey()]{ - _delegate->chooseSourceAccepted(QString::fromStdString(id)); - }, _sources.back()->lifetime()); + + 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; + _selectedId = QString::fromStdString(id); + if (_selectedId == _delegate->chooseSourceActiveDeviceId()) { + _selectedId = QString(); + _finish->setVisible(true); + _submit->setVisible(false); + } else { + _finish->setVisible(false); + _submit->setVisible(true); + } + }, raw->lifetime()); }; for (const auto &source : screensManager.sources()) { append(source); @@ -300,31 +466,34 @@ void ChooseSourceProcess::fillSources() { void ChooseSourceProcess::setupSourcesGeometry() { if (_sources.empty()) { - //LOG(()); destroy(); return; } _inner->widthValue( ) | rpl::start_with_next([=](int width) { const auto rows = int(std::ceil(_sources.size() / float(kColumns))); - const auto skip = st::desktopCaptureSourceSkip; - const auto single = (width - (kColumns + 1) * skip) / 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 = skip; + auto top = margins.top(); auto index = 0; for (auto row = 0; row != rows; ++row) { - auto left = skip; + 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 + skip; + left += single + skips.width(); } if (index >= _sources.size()) { break; } - top += height + skip; + top += height + skips.height(); } }, _inner->lifetime()); @@ -333,9 +502,10 @@ void ChooseSourceProcess::setupSourcesGeometry() { _scroll->heightValue() ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { const auto rows = int(std::ceil(_sources.size() / float(kColumns))); - const auto skip = st::desktopCaptureSourceSkip; + const auto margins = st::desktopCaptureMargins; + const auto skips = st::desktopCaptureSourceSkips; const auto height = st::desktopCaptureSourceSize.height(); - auto top = skip; + auto top = margins.top(); auto index = 0; for (auto row = 0; row != rows; ++row) { const auto hidden = (top + height <= scrollTop) @@ -353,7 +523,7 @@ void ChooseSourceProcess::setupSourcesGeometry() { if (index >= _sources.size()) { break; } - top += height + skip; + top += height + skips.height(); } }, _inner->lifetime()); } diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h index 22b871fa82..82f0fb9e5e 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h @@ -19,8 +19,10 @@ namespace Calls::Group::Ui::DesktopCapture { class ChooseSourceDelegate { public: virtual QWidget *chooseSourceParent() = 0; + virtual QString chooseSourceActiveDeviceId() = 0; virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0; virtual void chooseSourceAccepted(const QString &deviceId) = 0; + virtual void chooseSourceStop() = 0; }; void ChooseSource(not_null delegate); diff --git a/Telegram/SourceFiles/ui/image/image.cpp b/Telegram/SourceFiles/ui/image/image.cpp index 0dad5e9edd..dc98c2511c 100644 --- a/Telegram/SourceFiles/ui/image/image.cpp +++ b/Telegram/SourceFiles/ui/image/image.cpp @@ -93,16 +93,6 @@ QImage FromInlineBytes(const QByteArray &bytes) { return App::readImage(ExpandInlineBytes(bytes)); } -QSize GetSizeForDocument(const QVector &attributes) { - for (const auto &attribute : attributes) { - if (attribute.type() == mtpc_documentAttributeImageSize) { - auto &size = attribute.c_documentAttributeImageSize(); - return QSize(size.vw().v, size.vh().v); - } - } - return QSize(); -} - } // namespace Images Image::Image(const QString &path) : Image(ReadContent(path)) { diff --git a/Telegram/SourceFiles/ui/image/image.h b/Telegram/SourceFiles/ui/image/image.h index 07f5a6a4d3..f18f7bdbb1 100644 --- a/Telegram/SourceFiles/ui/image/image.h +++ b/Telegram/SourceFiles/ui/image/image.h @@ -14,9 +14,6 @@ namespace Images { [[nodiscard]] QByteArray ExpandInlineBytes(const QByteArray &bytes); [[nodiscard]] QImage FromInlineBytes(const QByteArray &bytes); -[[nodiscard]] QSize GetSizeForDocument( - const QVector &attributes); - } // namespace Images class Image final { diff --git a/Telegram/SourceFiles/ui/image/image_location_factory.cpp b/Telegram/SourceFiles/ui/image/image_location_factory.cpp index 57db9035c4..dc3776d57d 100644 --- a/Telegram/SourceFiles/ui/image/image_location_factory.cpp +++ b/Telegram/SourceFiles/ui/image/image_location_factory.cpp @@ -13,6 +13,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace Images { +namespace { + +QSize GetSizeForDocument(const QVector &attributes) { + for (const auto &attribute : attributes) { + if (attribute.type() == mtpc_documentAttributeImageSize) { + auto &size = attribute.c_documentAttributeImageSize(); + return QSize(size.vw().v, size.vh().v); + } + } + return QSize(); +} + +} // namespace ImageWithLocation FromPhotoSize( not_null session,