diff --git a/Telegram/Resources/icons/calls/call_cancel.png b/Telegram/Resources/icons/calls/call_cancel.png index 607033d0f..db5452f42 100644 Binary files a/Telegram/Resources/icons/calls/call_cancel.png and b/Telegram/Resources/icons/calls/call_cancel.png differ diff --git a/Telegram/Resources/icons/calls/call_cancel@2x.png b/Telegram/Resources/icons/calls/call_cancel@2x.png index 611f89d36..468337a51 100644 Binary files a/Telegram/Resources/icons/calls/call_cancel@2x.png and b/Telegram/Resources/icons/calls/call_cancel@2x.png differ diff --git a/Telegram/Resources/icons/calls/call_cancel@3x.png b/Telegram/Resources/icons/calls/call_cancel@3x.png index 006886953..2c9f6fac2 100644 Binary files a/Telegram/Resources/icons/calls/call_cancel@3x.png and b/Telegram/Resources/icons/calls/call_cancel@3x.png differ diff --git a/Telegram/Resources/icons/calls/mini_calls_arrow.png b/Telegram/Resources/icons/calls/mini_calls_arrow.png new file mode 100644 index 000000000..1ecb968db Binary files /dev/null and b/Telegram/Resources/icons/calls/mini_calls_arrow.png differ diff --git a/Telegram/Resources/icons/calls/mini_calls_arrow@2x.png b/Telegram/Resources/icons/calls/mini_calls_arrow@2x.png new file mode 100644 index 000000000..0effd8248 Binary files /dev/null and b/Telegram/Resources/icons/calls/mini_calls_arrow@2x.png differ diff --git a/Telegram/Resources/icons/calls/mini_calls_arrow@3x.png b/Telegram/Resources/icons/calls/mini_calls_arrow@3x.png new file mode 100644 index 000000000..87dd6a9cb Binary files /dev/null and b/Telegram/Resources/icons/calls/mini_calls_arrow@3x.png differ diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index a133bba68..56328c61d 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -155,6 +155,8 @@ callMicrophoneMute: CallButton(callAnswer) { bg: callIconBg; outerBg: callMuteRipple; label: callButtonLabel; + cornerButtonPosition: point(40px, 4px); + cornerButtonBorder: 2px; } callMicrophoneUnmute: CallButton(callMicrophoneMute) { button: IconButton(callButton) { @@ -181,6 +183,34 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) { } } } +callCornerButtonInner: IconButton { + width: 20px; + height: 20px; + + iconPosition: point(-1px, -1px); + + rippleAreaPosition: point(0px, 0px); + rippleAreaSize: 20px; + ripple: defaultRippleAnimation; +} +callCornerButton: CallButton(callMicrophoneMute) { + button: IconButton(callCornerButtonInner) { + icon: icon {{ "calls/mini_calls_arrow", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callMuteRipple; + } + } + bgSize: 20px; + bgPosition: point(0px, 0px); +} +callCornerButtonInactive: CallButton(callMicrophoneUnmute, callCornerButton) { + button: IconButton(callCornerButtonInner) { + icon: icon {{ "calls/mini_calls_arrow", callIconFgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callIconActiveRipple; + } + } +} callScreencastOn: CallButton(callMicrophoneMute) { button: IconButton(callButton) { icon: icon {{ "calls/calls_present", callIconFg }}; @@ -576,6 +606,18 @@ groupCallMenuAbout: FlatLabel(defaultFlatLabel) { minWidth: 200px; maxHeight: 92px; } +callDeviceSelectionLabel: FlatLabel(defaultSubsectionTitle) { + textFg: groupCallActiveFg; + minWidth: 200px; + maxHeight: 20px; +} +callDeviceSelectionMenu: PopupMenu(groupCallPopupMenu) { + scrollPadding: margins(0px, 3px, 0px, 8px); + menu: Menu(groupCallMenu) { + widthMin: 240px; + itemPadding: margins(17px, 8px, 17px, 7px); + } +} groupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px); groupCallRecordingTimerFont: font(12px); diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 0ca37c629..3a34f0783 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -1310,6 +1310,19 @@ void Call::toggleScreenSharing(std::optional uniqueId) { _videoOutgoing->setState(Webrtc::VideoState::Active); } +auto Call::playbackDeviceIdValue() const +-> rpl::producer { + return _playbackDeviceId.value(); +} + +rpl::producer Call::captureDeviceIdValue() const { + return _captureDeviceId.value(); +} + +rpl::producer Call::cameraDeviceIdValue() const { + return _cameraDeviceId.value(); +} + void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) { Expects(type != FinishType::None); diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 0a99c8067..1d01d6d5f 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -31,6 +31,7 @@ enum class AudioState; namespace Webrtc { enum class VideoState; class VideoTrack; +struct DeviceResolvedId; } // namespace Webrtc namespace Calls { @@ -220,6 +221,13 @@ public: void toggleCameraSharing(bool enabled); void toggleScreenSharing(std::optional uniqueId); + [[nodiscard]] auto playbackDeviceIdValue() const + -> rpl::producer; + [[nodiscard]] auto captureDeviceIdValue() const + -> rpl::producer; + [[nodiscard]] auto cameraDeviceIdValue() const + -> rpl::producer; + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 1a442cfc2..c3f94e0ec 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_file.h" #include "data/data_changes.h" #include "calls/group/calls_group_common.h" +#include "calls/ui/calls_device_menu.h" #include "calls/calls_emoji_fingerprint.h" #include "calls/calls_signal_bars.h" #include "calls/calls_userpic.h" @@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/call_button.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "ui/widgets/rp_window.h" #include "ui/layers/layer_manager.h" @@ -130,6 +132,7 @@ Panel::Panel(not_null call) initWidget(); initControls(); initLayout(); + initMediaDeviceToggles(); showAndActivate(); } @@ -736,6 +739,58 @@ void Panel::initGeometry() { updateControlsGeometry(); } +void Panel::initMediaDeviceToggles() { + _cameraDeviceToggle = _camera->addCornerButton( + st::callCornerButton, + &st::callCornerButtonInactive); + _audioDeviceToggle = _mute->entity()->addCornerButton( + st::callCornerButton, + &st::callCornerButtonInactive); + + _cameraDeviceToggle->setClickedCallback([=] { + showDevicesMenu(_cameraDeviceToggle, { + { Webrtc::DeviceType::Camera, _call->cameraDeviceIdValue() }, + }); + }); + _audioDeviceToggle->setClickedCallback([=] { + showDevicesMenu(_audioDeviceToggle, { + { Webrtc::DeviceType::Playback, _call->playbackDeviceIdValue() }, + { Webrtc::DeviceType::Capture, _call->captureDeviceIdValue() }, + }); + }); +} + +void Panel::showDevicesMenu( + not_null button, + std::vector types) { + if (!_call || _devicesMenu) { + return; + } + const auto chosen = [=](Webrtc::DeviceType type, QString id) { + switch (type) { + case Webrtc::DeviceType::Playback: + Core::App().settings().setCallPlaybackDeviceId(id); + break; + case Webrtc::DeviceType::Capture: + Core::App().settings().setCallCaptureDeviceId(id); + break; + case Webrtc::DeviceType::Camera: + Core::App().settings().setCameraDeviceId(id); + break; + } + Core::App().saveSettingsDelayed(); + }; + _devicesMenu = MakeDeviceSelectionMenu( + widget(), + &Core::App().mediaDevices(), + std::move(types), + chosen); + _devicesMenu->setForcedVerticalOrigin( + Ui::PopupMenu::VerticalOrigin::Bottom); + _devicesMenu->popup(button->mapToGlobal(QPoint()) + - QPoint(st::callDeviceSelectionMenu.menu.widthMin / 2, 0)); +} + void Panel::refreshOutgoingPreviewInBody(State state) { const auto inBody = (state != State::Established) && (_call->videoOutgoing()->state() != Webrtc::VideoState::Inactive) diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 11c6dceae..e53045a44 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -37,6 +37,7 @@ class FadeWrap; template class PaddingWrap; class RpWindow; +class PopupMenu; namespace GL { enum class Backend; } // namespace GL @@ -55,6 +56,7 @@ namespace Calls { class Userpic; class SignalBars; class VideoBubble; +struct DeviceSelection; class Panel final : private Group::Ui::DesktopCapture::ChooseSourceDelegate { public: @@ -104,6 +106,7 @@ private: void initControls(); void reinitWithCall(Call *call); void initLayout(); + void initMediaDeviceToggles(); void initGeometry(); [[nodiscard]] bool handleClose() const; @@ -126,6 +129,10 @@ private: void showRemoteLowBattery(); void refreshAnswerHangupRedialLabel(); + void showDevicesMenu( + not_null button, + std::vector types); + [[nodiscard]] QRect incomingFrameGeometry() const; [[nodiscard]] QRect outgoingFrameGeometry() const; @@ -156,8 +163,10 @@ private: Ui::Animations::Simple _hangupShownProgress; object_ptr> _screencast; object_ptr _camera; + Ui::CallButton *_cameraDeviceToggle = nullptr; base::unique_qptr _startVideo; object_ptr> _mute; + Ui::CallButton *_audioDeviceToggle = nullptr; object_ptr _name; object_ptr _status; object_ptr _fingerprint = { nullptr }; @@ -170,6 +179,8 @@ private: int _bodyTop = 0; int _buttonsTop = 0; + base::unique_qptr _devicesMenu; + base::Timer _updateDurationTimer; base::Timer _updateOuterRippleTimer; diff --git a/Telegram/SourceFiles/calls/ui/calls_device_menu.cpp b/Telegram/SourceFiles/calls/ui/calls_device_menu.cpp new file mode 100644 index 000000000..1db1941c0 --- /dev/null +++ b/Telegram/SourceFiles/calls/ui/calls_device_menu.cpp @@ -0,0 +1,250 @@ +/* +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/ui/calls_device_menu.h" + +#include "lang/lang_keys.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/vertical_layout.h" +#include "webrtc/webrtc_device_common.h" +#include "webrtc/webrtc_environment.h" +#include "styles/style_calls.h" +#include "styles/style_layers.h" + +namespace Calls { +namespace { + +class Subsection final : public Ui::Menu::ItemBase { +public: + Subsection( + not_null parent, + const style::Menu &st, + const QString &text); + + not_null action() const override; + bool isEnabled() const override; + +private: + int contentHeight() const override; + + const style::Menu &_st; + const base::unique_qptr _text; + const not_null _dummyAction; + +}; + +class Selector final : public Ui::Menu::ItemBase { +public: + Selector( + not_null parent, + const style::Menu &st, + rpl::producer> devices, + rpl::producer chosen, + Fn selected); + + not_null action() const override; + bool isEnabled() const override; + +private: + int contentHeight() const override; + [[nodiscard]] int registerId(const QString &id); + + const base::unique_qptr _scroll; + const not_null _list; + const not_null _dummyAction; + + base::flat_map _ids; + +}; + +Subsection::Subsection( + not_null parent, + const style::Menu &st, + const QString &text) +: Ui::Menu::ItemBase(parent, st) +, _st(st) +, _text(base::make_unique_q( + this, + text, + st::callDeviceSelectionLabel)) +, _dummyAction(new QAction(parent)) { + setPointerCursor(false); + + initResizeHook(parent->sizeValue()); + + _text->resizeToWidth(st::callDeviceSelectionLabel.minWidth); + _text->moveToLeft(st.itemPadding.left(), st.itemPadding.top()); +} + +not_null Subsection::action() const { + return _dummyAction; +} + +bool Subsection::isEnabled() const { + return false; +} + +int Subsection::contentHeight() const { + return _st.itemPadding.top() + + _text->height() + + _st.itemPadding.bottom(); +} + +Selector::Selector( + not_null parent, + const style::Menu &st, + rpl::producer> devices, + rpl::producer chosen, + Fn selected) +: Ui::Menu::ItemBase(parent, st) +, _scroll(base::make_unique_q(this)) +, _list(_scroll->setOwnedWidget(object_ptr(this))) +, _dummyAction(new QAction(parent)) { + setPointerCursor(false); + + initResizeHook(parent->sizeValue()); + + const auto padding = st.itemPadding; + const auto group = std::make_shared(); + std::move( + chosen + ) | rpl::start_with_next([=](Webrtc::DeviceResolvedId id) { + const auto value = id.isDefault() ? 0 : registerId(id.value); + if (!group->hasValue() || group->current() != value) { + group->setValue(value); + } + }, lifetime()); + + group->setChangedCallback([=](int value) { + if (value == 0) { + selected({}); + } else { + for (const auto &[id, index] : _ids) { + if (index == value) { + selected(id); + break; + } + } + } + }); + + std::move( + devices + ) | rpl::start_with_next([=](const std::vector &v) { + while (_list->count()) { + delete _list->widgetAt(0); + } + _list->add( + object_ptr( + _list.get(), + group, + 0, + tr::lng_settings_call_device_default(tr::now), + st::groupCallCheckbox, + st::groupCallRadio), + padding); + for (const auto &device : v) { + if (device.inactive) { + continue; + } + _list->add( + object_ptr( + _list.get(), + group, + registerId(device.id), + device.name, + st::groupCallCheckbox, + st::groupCallRadio), + padding); + } + resize(width(), contentHeight()); + }, lifetime()); +} + +not_null Selector::action() const { + return _dummyAction; +} + +bool Selector::isEnabled() const { + return false; +} + +int Selector::contentHeight() const { + _list->resizeToWidth(width()); + if (_list->count() <= 3) { + _scroll->resize(width(), _list->height()); + } else { + _scroll->resize( + width(), + 3.5 * st::defaultRadio.diameter); + } + return _scroll->height(); +} + +int Selector::registerId(const QString &id) { + auto &result = _ids[id]; + if (!result) { + result = int(_ids.size()); + } + return result; +} + +void AddDeviceSelection( + not_null menu, + not_null environment, + DeviceSelection type, + Fn selected) { + const auto title = [&] { + switch (type.type) { + case Webrtc::DeviceType::Camera: + return tr::lng_settings_call_camera(tr::now); + case Webrtc::DeviceType::Playback: + return tr::lng_settings_call_section_output(tr::now); + case Webrtc::DeviceType::Capture: + return tr::lng_settings_call_section_input(tr::now); + } + Unexpected("Type in AddDeviceSelection."); + }(); + menu->addAction( + base::make_unique_q(menu, menu->st().menu, title)); + menu->addAction( + base::make_unique_q( + menu, + menu->st().menu, + environment->devicesValue(type.type), + std::move(type.chosen), + selected)); +} + +} // namespace + +base::unique_qptr MakeDeviceSelectionMenu( + not_null parent, + not_null environment, + std::vector types, + Fn choose) { + auto result = base::make_unique_q( + parent, + st::callDeviceSelectionMenu); + const auto raw = result.get(); + for (auto type : types) { + if (!raw->empty()) { + raw->addSeparator(); + } + const auto selected = [=, type = type.type](QString id) { + choose(type, id); + }; + AddDeviceSelection(raw, environment, std::move(type), selected); + } + return result; +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/ui/calls_device_menu.h b/Telegram/SourceFiles/calls/ui/calls_device_menu.h new file mode 100644 index 000000000..bff2fcc42 --- /dev/null +++ b/Telegram/SourceFiles/calls/ui/calls_device_menu.h @@ -0,0 +1,36 @@ +/* +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 +*/ +#pragma once + +#include "base/unique_qptr.h" + +namespace Webrtc { +class Environment; +struct DeviceResolvedId; +enum class DeviceType : uchar; +} // namespace Webrtc + +namespace Ui { +class RpWidget; +class PopupMenu; +} // namespace Ui + +namespace Calls { + +struct DeviceSelection { + Webrtc::DeviceType type; + rpl::producer chosen; +}; + +[[nodiscard]] base::unique_qptr MakeDeviceSelectionMenu( + not_null parent, + not_null environment, + std::vector types, + Fn choose); + +} // namespace Calls diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index ca583b017..8ec670702 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -64,6 +64,8 @@ PRIVATE calls/group/ui/calls_group_scheduled_labels.h calls/group/ui/desktop_capture_choose_source.cpp calls/group/ui/desktop_capture_choose_source.h + calls/ui/calls_device_menu.cpp + calls/ui/calls_device_menu.h chat_helpers/field_characters_count_manager.cpp chat_helpers/field_characters_count_manager.h diff --git a/Telegram/lib_ui b/Telegram/lib_ui index fc5386f1f..47ec1b045 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit fc5386f1fd4a17fafa88f1bca544e92c4c0ddf99 +Subproject commit 47ec1b0455ac1f2faab68a4c859baab7eef9e136