From ca5e18391b38864b201c873ebf32d201363e01a7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 14 Jan 2026 15:45:31 +0400 Subject: [PATCH] Rewrite settings_calls to new builder pattern Co-Authored-By: Claude Opus 4.5 --- Telegram/CMakeLists.txt | 4 +- .../calls/calls_box_controller.cpp | 2 +- .../calls/group/calls_group_settings.cpp | 2 +- .../core/deep_links/deep_links_settings.cpp | 2 +- .../{ => sections}/settings_calls.cpp | 849 +++++++++++------- .../settings/{ => sections}/settings_calls.h | 41 +- .../settings/sections/settings_main.cpp | 2 +- .../ui/controls/userpic_button.cpp | 2 +- .../SourceFiles/window/window_main_menu.cpp | 2 +- 9 files changed, 538 insertions(+), 368 deletions(-) rename Telegram/SourceFiles/settings/{ => sections}/settings_calls.cpp (63%) rename Telegram/SourceFiles/settings/{ => sections}/settings_calls.h (81%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 86a3b29f44..0048817882 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1554,8 +1554,8 @@ PRIVATE settings/settings_blocked_peers.h settings/settings_business.cpp settings/settings_business.h - settings/settings_calls.cpp - settings/settings_calls.h + settings/sections/settings_calls.cpp + settings/sections/settings_calls.h settings/settings_codes.cpp settings/settings_codes.h settings/settings_common_session.cpp diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index 19ec51f14d..a457d369a0 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -42,7 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "info/profile/info_profile_icon.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" #include "settings/settings_common.h" #include "styles/style_info.h" // infoTopBarMenu #include "styles/style_layers.h" // st::boxLabel. diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 810b798e11..881ee38052 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -44,7 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_device_resolver.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" #include "settings/settings_credits_graphics.h" #include "main/main_session.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp b/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp index 809a5a5ce7..1dc546bcc6 100644 --- a/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp +++ b/Telegram/SourceFiles/core/deep_links/deep_links_settings.cpp @@ -42,7 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/sections/settings_advanced.h" #include "settings/settings_blocked_peers.h" #include "settings/settings_business.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" #include "settings/sections/settings_chat.h" #include "settings/settings_passkeys.h" #include "data/components/passkeys.h" diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/sections/settings_calls.cpp similarity index 63% rename from Telegram/SourceFiles/settings/settings_calls.cpp rename to Telegram/SourceFiles/settings/sections/settings_calls.cpp index c194267dfe..2a11d33944 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_calls.cpp @@ -5,41 +5,47 @@ 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 "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" -#include "ui/wrap/vertical_layout.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/level_meter.h" -#include "ui/widgets/buttons.h" -#include "ui/boxes/single_choice_box.h" -#include "ui/boxes/confirm_box.h" -#include "ui/vertical_list.h" -#include "platform/platform_specific.h" -#include "main/main_session.h" -#include "lang/lang_keys.h" -#include "styles/style_settings.h" -#include "ui/widgets/continuous_sliders.h" -#include "window/window_session_controller.h" -#include "core/application.h" -#include "core/core_settings.h" +#include "api/api_authorizations.h" +#include "apiwrap.h" +#include "base/timer.h" #include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_video_bubble.h" -#include "apiwrap.h" -#include "api/api_authorizations.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "platform/platform_specific.h" +#include "settings/sections/settings_main.h" +#include "settings/settings_builder.h" +#include "tgcalls/VideoCaptureInterface.h" +#include "ui/boxes/confirm_box.h" +#include "ui/boxes/single_choice_box.h" +#include "ui/effects/animations.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/level_meter.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "webrtc/webrtc_audio_input_tester.h" +#include "webrtc/webrtc_create_adm.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" -#include "webrtc/webrtc_audio_input_tester.h" -#include "webrtc/webrtc_create_adm.h" // Webrtc::Backend. -#include "tgcalls/VideoCaptureInterface.h" +#include "window/window_session_controller.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" namespace Settings { namespace { using namespace Webrtc; +using namespace Builder; [[nodiscard]] rpl::producer DeviceNameValue( DeviceType type, @@ -56,274 +62,8 @@ using namespace Webrtc; }) | rpl::flatten_latest(); } -} // namespace - -Calls::Calls( - QWidget *parent, - not_null controller) -: Section(parent, controller) { - // Request valid value of calls disabled flag. - controller->session().api().authorizations().reload(); - - setupContent(); - requestPermissionAndStartTestingMicrophone(); -} - -Calls::~Calls() = default; - -rpl::producer Calls::title() { - return tr::lng_settings_section_devices(); -} - -Webrtc::VideoTrack *Calls::AddCameraSubsection( - std::shared_ptr show, - not_null content, - bool saveToSettings) { - auto &lifetime = content->lifetime(); - - const auto hasCall = (Core::App().calls().currentCall() != nullptr); - - auto capturerOwner = lifetime.make_state< - std::shared_ptr - >(); - - const auto track = lifetime.make_state( - (hasCall - ? VideoState::Inactive - : VideoState::Active)); - - const auto deviceId = lifetime.make_state>( - Core::App().settings().cameraDeviceId()); - auto resolvedId = rpl::deferred([=] { - return DeviceIdOrDefault(deviceId->value()); - }); - AddButtonWithLabel( - content, - tr::lng_settings_call_input_device(), - CameraDeviceNameValue(rpl::duplicate(resolvedId)), - st::settingsButtonNoIcon - )->addClickHandler([=] { - show->show(ChooseCameraDeviceBox( - rpl::duplicate(resolvedId), - [=](const QString &id) { - *deviceId = id; - if (saveToSettings) { - Core::App().settings().setCameraDeviceId(id); - Core::App().saveSettingsDelayed(); - } - if (*capturerOwner) { - (*capturerOwner)->switchToDevice( - id.toStdString(), - false); - } - })); - }); - const auto bubbleWrap = content->add(object_ptr(content)); - const auto bubble = lifetime.make_state<::Calls::VideoBubble>( - bubbleWrap, - track); - const auto padding = st::settingsButtonNoIcon.padding.left(); - const auto top = st::boxRoundShadow.extend.top(); - const auto bottom = st::boxRoundShadow.extend.bottom(); - - auto frameSize = track->renderNextFrame( - ) | rpl::map([=] { - return track->frameSize(); - }) | rpl::filter([=](QSize size) { - return !size.isEmpty() - && !Core::App().calls().currentCall() - && !Core::App().calls().currentGroupCall(); - }); - auto bubbleWidth = bubbleWrap->widthValue( - ) | rpl::filter([=](int width) { - return width > 2 * padding + 1; - }); - rpl::combine( - std::move(bubbleWidth), - std::move(frameSize) - ) | rpl::on_next([=](int width, QSize frame) { - const auto useWidth = (width - 2 * padding); - const auto useHeight = std::min( - ((useWidth * frame.height()) / frame.width()), - (useWidth * 480) / 640); - bubbleWrap->resize(width, top + useHeight + bottom); - bubble->updateGeometry( - ::Calls::VideoBubble::DragMode::None, - QRect(padding, top, useWidth, useHeight)); - bubbleWrap->update(); - }, bubbleWrap->lifetime()); - - using namespace rpl::mappers; - const auto checkCapturer = [=] { - if (*capturerOwner - || Core::App().calls().currentCall() - || Core::App().calls().currentGroupCall()) { - return; - } - *capturerOwner = Core::App().calls().getVideoCapture( - Core::App().settings().cameraDeviceId(), - false); - (*capturerOwner)->setPreferredAspectRatio(0.); - track->setState(VideoState::Active); - (*capturerOwner)->setState(tgcalls::VideoState::Active); - (*capturerOwner)->setOutput(track->sink()); - }; - rpl::combine( - Core::App().calls().currentCallValue(), - Core::App().calls().currentGroupCallValue(), - _1 || _2 - ) | rpl::on_next([=](bool has) { - if (has) { - track->setState(VideoState::Inactive); - bubbleWrap->resize(bubbleWrap->width(), 0); - *capturerOwner = nullptr; - } else { - crl::on_main(content, checkCapturer); - } - }, lifetime); - - return track; -} - -void Calls::sectionSaveChanges(FnMut done) { - _testingMicrophone = false; - done(); -} - -void Calls::setupContent() { - const auto content = Ui::CreateChild(this); - const auto settings = &Core::App().settings(); - - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_output()); - - initPlaybackButton( - content, - tr::lng_settings_call_output_device(), - rpl::deferred([=] { - return DeviceIdOrDefault(settings->playbackDeviceIdValue()); - }), - [=](const QString &id) { settings->setPlaybackDeviceId(id); }); - - Ui::AddSkip(content); - Ui::AddDivider(content); - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_input()); - initCaptureButton( - content, - tr::lng_settings_call_input_device(), - rpl::deferred([=] { - return DeviceIdOrDefault(settings->captureDeviceIdValue()); - }), - [=](const QString &id) { settings->setCaptureDeviceId(id); }); - - Ui::AddSkip(content); - Ui::AddDivider(content); - - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_settings_devices_calls()); - const auto orDefault = [](const QString &value) { - return value.isEmpty() ? kDefaultDeviceId : value; - }; - const auto same = content->add(object_ptr( - content, - tr::lng_settings_devices_calls_same(), - st::settingsButtonNoIcon)); - same->toggleOn(rpl::combine( - settings->callPlaybackDeviceIdValue(), - settings->callCaptureDeviceIdValue() - ) | rpl::map([](const QString &playback, const QString &capture) { - return playback.isEmpty() && capture.isEmpty(); - })); - same->toggledValue() | rpl::filter([=](bool toggled) { - const auto empty = settings->callPlaybackDeviceId().isEmpty() - && settings->callCaptureDeviceId().isEmpty(); - return (empty != toggled); - }) | rpl::on_next([=](bool toggled) { - if (toggled) { - settings->setCallPlaybackDeviceId(QString()); - settings->setCallCaptureDeviceId(QString()); - } else { - settings->setCallPlaybackDeviceId( - orDefault(settings->playbackDeviceId())); - settings->setCallCaptureDeviceId( - orDefault(settings->captureDeviceId())); - } - Core::App().saveSettingsDelayed(); - }, same->lifetime()); - const auto different = content->add( - object_ptr>( - content, - object_ptr(content))); - const auto calls = different->entity(); - initPlaybackButton( - calls, - tr::lng_group_call_speakers(), - rpl::deferred([=] { - return DeviceIdValueWithFallback( - settings->callPlaybackDeviceIdValue(), - settings->playbackDeviceIdValue()); - }), - [=](const QString &id) { settings->setCallPlaybackDeviceId(id); }); - initCaptureButton( - calls, - tr::lng_group_call_microphone(), - rpl::deferred([=] { - return DeviceIdValueWithFallback( - settings->callCaptureDeviceIdValue(), - settings->captureDeviceIdValue()); - }), - [=](const QString &id) { settings->setCallCaptureDeviceId(id); }); - different->toggleOn(same->toggledValue() | rpl::map(!rpl::mappers::_1)); - Ui::AddSkip(content); - Ui::AddDivider(content); - - if (!Core::App().mediaDevices().defaultId( - Webrtc::DeviceType::Camera).isEmpty()) { - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_settings_call_camera()); - AddCameraSubsection(controller()->uiShow(), content, true); - Ui::AddSkip(content); - Ui::AddDivider(content); - } - - Ui::AddSkip(content); - Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_other()); - - const auto api = &controller()->session().api(); - content->add(object_ptr( - content, - tr::lng_settings_call_accept_calls(), - st::settingsButtonNoIcon - ))->toggleOn( - api->authorizations().callsDisabledHereValue( - ) | rpl::map(!rpl::mappers::_1) - )->toggledChanges( - ) | rpl::filter([=](bool value) { - return (value == api->authorizations().callsDisabledHere()); - }) | on_next([=](bool value) { - api->authorizations().toggleCallsDisabledHere(!value); - }, content->lifetime()); - - content->add(object_ptr( - content, - tr::lng_settings_call_open_system_prefs(), - st::settingsButtonNoIcon - ))->addClickHandler([=] { - using namespace ::Platform; - const auto opened = OpenSystemSettings(SystemSettingsType::Audio); - if (!opened) { - controller()->show( - Ui::MakeInformBox(tr::lng_linux_no_audio_prefs())); - } - }); - - Ui::AddSkip(content); - - Ui::ResizeFitChild(this, content); -} - -void Calls::initPlaybackButton( +void InitPlaybackButton( + not_null controller, not_null container, rpl::producer text, rpl::producer resolvedId, @@ -334,28 +74,29 @@ void Calls::initPlaybackButton( PlaybackDeviceNameValue(rpl::duplicate(resolvedId)), st::settingsButtonNoIcon )->addClickHandler([=] { - controller()->show(ChoosePlaybackDeviceBox( + controller->show(ChoosePlaybackDeviceBox( rpl::duplicate(resolvedId), [=](const QString &id) { set(id); Core::App().saveSettingsDelayed(); })); }); - } -void Calls::initCaptureButton( +void InitCaptureButton( + not_null controller, not_null container, rpl::producer text, rpl::producer resolvedId, - Fn set) { + Fn set, + rpl::variable *testingMicrophone) { AddButtonWithLabel( container, tr::lng_settings_call_input_device(), CaptureDeviceNameValue(rpl::duplicate(resolvedId)), st::settingsButtonNoIcon )->addClickHandler([=] { - controller()->show(ChooseCaptureDeviceBox( + controller->show(ChooseCaptureDeviceBox( rpl::duplicate(resolvedId), [=](const QString &id) { set(id); @@ -385,7 +126,7 @@ void Calls::initCaptureButton( level->setValue(state->animation.value(state->level)); }, was, state->level, kMicTestAnimationDuration); }); - _testingMicrophone.value() | rpl::on_next([=](bool testing) { + testingMicrophone->value() | rpl::on_next([=](bool testing) { if (testing) { state->deviceId = std::make_unique( &Core::App().mediaDevices(), @@ -403,52 +144,301 @@ void Calls::initCaptureButton( }, level->lifetime()); } -void Calls::requestPermissionAndStartTestingMicrophone() { - using PermissionType = ::Platform::PermissionType; - using PermissionStatus = ::Platform::PermissionStatus; - const auto status = GetPermissionStatus( - PermissionType::Microphone); - if (status == PermissionStatus::Granted) { - _testingMicrophone = true; - } else if (status == PermissionStatus::CanRequest) { - const auto startTestingChecked = crl::guard(this, [=]( - PermissionStatus status) { - if (status == PermissionStatus::Granted) { - crl::on_main(crl::guard(this, [=] { - _testingMicrophone = true; - })); - } - }); - RequestPermission( - PermissionType::Microphone, - startTestingChecked); - } else { - const auto showSystemSettings = [controller = controller()] { - OpenSystemSettingsForPermission( - PermissionType::Microphone); - controller->hideLayer(); - }; - controller()->show(Ui::MakeConfirmBox({ - .text = tr::lng_no_mic_permission(), - .confirmed = showSystemSettings, - .confirmText = tr::lng_menu_settings(), - })); +void BuildOutputSection(SectionBuilder &builder) { + const auto controller = builder.controller(); + if (!controller) { + return; } + const auto settings = &Core::App().settings(); + + builder.addSkip(); + builder.addSubsectionTitle({ + .id = u"calls/output"_q, + .title = tr::lng_settings_call_section_output(), + .keywords = { u"speakers"_q, u"output"_q, u"audio"_q }, + }); + + builder.add([controller, settings](const WidgetContext &ctx) { + InitPlaybackButton( + controller, + ctx.container, + tr::lng_settings_call_output_device(), + rpl::deferred([=] { + return DeviceIdOrDefault(settings->playbackDeviceIdValue()); + }), + [=](const QString &id) { settings->setPlaybackDeviceId(id); }); + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"calls/output/device"_q, + .title = tr::lng_settings_call_output_device(tr::now), + .keywords = { u"speakers"_q, u"output"_q, u"playback"_q }, + }; + }); } -rpl::producer PlaybackDeviceNameValue(rpl::producer id) { - return DeviceNameValue(DeviceType::Playback, std::move(id)); +void BuildInputSection( + SectionBuilder &builder, + rpl::variable *testingMicrophone) { + const auto controller = builder.controller(); + if (!controller) { + return; + } + const auto settings = &Core::App().settings(); + + builder.addSkip(); + builder.addDivider(); + builder.addSkip(); + builder.addSubsectionTitle({ + .id = u"calls/input"_q, + .title = tr::lng_settings_call_section_input(), + .keywords = { u"microphone"_q, u"input"_q, u"audio"_q }, + }); + + builder.add([controller, settings, testingMicrophone](const WidgetContext &ctx) { + InitCaptureButton( + controller, + ctx.container, + tr::lng_settings_call_input_device(), + rpl::deferred([=] { + return DeviceIdOrDefault(settings->captureDeviceIdValue()); + }), + [=](const QString &id) { settings->setCaptureDeviceId(id); }, + testingMicrophone); + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"calls/input/device"_q, + .title = tr::lng_settings_call_input_device(tr::now), + .keywords = { u"microphone"_q, u"input"_q, u"capture"_q }, + }; + }); } -rpl::producer CaptureDeviceNameValue(rpl::producer id) { - return DeviceNameValue(DeviceType::Capture, std::move(id)); +void BuildCallDevicesSection(SectionBuilder &builder) { + const auto controller = builder.controller(); + if (!controller) { + return; + } + const auto settings = &Core::App().settings(); + + builder.addSkip(); + builder.addDivider(); + builder.addSkip(); + builder.addSubsectionTitle({ + .id = u"calls/devices"_q, + .title = tr::lng_settings_devices_calls(), + .keywords = { u"calls"_q, u"devices"_q, u"same"_q }, + }); + + const auto orDefault = [](const QString &value) { + return value.isEmpty() ? kDefaultDeviceId : value; + }; + + const auto same = builder.addButton({ + .id = u"calls/same-devices"_q, + .title = tr::lng_settings_devices_calls_same(), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::combine( + settings->callPlaybackDeviceIdValue(), + settings->callCaptureDeviceIdValue() + ) | rpl::map([](const QString &playback, const QString &capture) { + return playback.isEmpty() && capture.isEmpty(); + }), + .keywords = { u"same"_q, u"separate"_q, u"devices"_q }, + }); + + if (same) { + same->toggledValue() | rpl::filter([=](bool toggled) { + const auto empty = settings->callPlaybackDeviceId().isEmpty() + && settings->callCaptureDeviceId().isEmpty(); + return (empty != toggled); + }) | rpl::on_next([=](bool toggled) { + if (toggled) { + settings->setCallPlaybackDeviceId(QString()); + settings->setCallCaptureDeviceId(QString()); + } else { + settings->setCallPlaybackDeviceId( + orDefault(settings->playbackDeviceId())); + settings->setCallCaptureDeviceId( + orDefault(settings->captureDeviceId())); + } + Core::App().saveSettingsDelayed(); + }, same->lifetime()); + } + + builder.add([controller, settings, same, orDefault](const WidgetContext &ctx) { + const auto container = ctx.container.get(); + const auto different = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto calls = different->entity(); + + InitPlaybackButton( + controller, + calls, + tr::lng_group_call_speakers(), + rpl::deferred([=] { + return DeviceIdValueWithFallback( + settings->callPlaybackDeviceIdValue(), + settings->playbackDeviceIdValue()); + }), + [=](const QString &id) { settings->setCallPlaybackDeviceId(id); }); + + struct LevelStateStub { + std::unique_ptr deviceId; + std::unique_ptr tester; + base::Timer timer; + Ui::Animations::Simple animation; + float level = 0.; + }; + const auto captureId = rpl::deferred([=] { + return DeviceIdValueWithFallback( + settings->callCaptureDeviceIdValue(), + settings->captureDeviceIdValue()); + }); + AddButtonWithLabel( + calls, + tr::lng_group_call_microphone(), + CaptureDeviceNameValue(rpl::duplicate(captureId)), + st::settingsButtonNoIcon + )->addClickHandler([=] { + controller->show(ChooseCaptureDeviceBox( + rpl::duplicate(captureId), + [=](const QString &id) { + settings->setCallCaptureDeviceId(id); + Core::App().saveSettingsDelayed(); + })); + }); + + if (same) { + different->toggleOn( + same->toggledValue() | rpl::map(!rpl::mappers::_1)); + } + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"calls/call-speakers"_q, + .title = tr::lng_group_call_speakers(tr::now), + .keywords = { u"speakers"_q, u"calls"_q }, + }; + }); + builder.add(nullptr, [] { + return SearchEntry{ + .id = u"calls/call-microphone"_q, + .title = tr::lng_group_call_microphone(tr::now), + .keywords = { u"microphone"_q, u"calls"_q }, + }; + }); } -rpl::producer CameraDeviceNameValue( - rpl::producer id) { - return DeviceNameValue(DeviceType::Camera, std::move(id)); +void BuildCameraSection(SectionBuilder &builder) { + const auto controller = builder.controller(); + if (!controller) { + return; + } + + if (Core::App().mediaDevices().defaultId( + Webrtc::DeviceType::Camera).isEmpty()) { + return; + } + + builder.addSkip(); + builder.addDivider(); + builder.addSkip(); + builder.addSubsectionTitle({ + .id = u"calls/camera"_q, + .title = tr::lng_settings_call_camera(), + .keywords = { u"camera"_q, u"video"_q, u"webcam"_q }, + }); + + builder.add([controller](const WidgetContext &ctx) { + Calls::AddCameraSubsection(controller->uiShow(), ctx.container, true); + return SectionBuilder::WidgetToAdd{}; + }, [] { + return SearchEntry{ + .id = u"calls/camera/device"_q, + .title = tr::lng_settings_call_input_device(tr::now), + .keywords = { u"camera"_q, u"video"_q, u"webcam"_q }, + }; + }); } +void BuildOtherSection(SectionBuilder &builder) { + const auto controller = builder.controller(); + const auto session = builder.session(); + + builder.addSkip(); + builder.addDivider(); + builder.addSkip(); + builder.addSubsectionTitle({ + .id = u"calls/other"_q, + .title = tr::lng_settings_call_section_other(), + .keywords = { u"calls"_q, u"accept"_q, u"system"_q }, + }); + + const auto api = &session->api(); + const auto authorizations = &api->authorizations(); + authorizations->reload(); + + const auto acceptCalls = builder.addButton({ + .id = u"calls/accept"_q, + .title = tr::lng_settings_call_accept_calls(), + .st = &st::settingsButtonNoIcon, + .toggled = authorizations->callsDisabledHereValue() + | rpl::map(!rpl::mappers::_1), + .keywords = { u"accept"_q, u"receive"_q, u"incoming"_q }, + }); + + if (acceptCalls) { + acceptCalls->toggledChanges( + ) | rpl::filter([=](bool value) { + return (value == authorizations->callsDisabledHere()); + }) | rpl::on_next([=](bool value) { + authorizations->toggleCallsDisabledHere(!value); + }, acceptCalls->lifetime()); + } + + builder.addButton({ + .id = u"calls/system-prefs"_q, + .title = tr::lng_settings_call_open_system_prefs(), + .st = &st::settingsButtonNoIcon, + .onClick = [controller] { + using namespace ::Platform; + const auto opened = OpenSystemSettings(SystemSettingsType::Audio); + if (!opened) { + controller->show( + Ui::MakeInformBox(tr::lng_linux_no_audio_prefs())); + } + }, + .keywords = { u"system"_q, u"preferences"_q, u"audio"_q }, + }); + + builder.addSkip(); +} + +void BuildCallsSectionContent(SectionBuilder &builder) { + auto *testingMicrophone = builder.container() + ? builder.container()->lifetime().make_state>() + : nullptr; + + BuildOutputSection(builder); + BuildInputSection(builder, testingMicrophone); + BuildCallDevicesSection(builder); + BuildCameraSection(builder); + BuildOtherSection(builder); +} + +const auto kMeta = BuildHelper({ + .id = Calls::Id(), + .parentId = Main::Id(), + .title = &tr::lng_settings_section_devices, + .icon = &st::menuIconUnmute, +}, [](SectionBuilder &builder) { + BuildCallsSectionContent(builder); +}); + void ChooseMediaDeviceBox( not_null box, rpl::producer title, @@ -642,6 +632,191 @@ void ChooseMediaDeviceBox( }); } +} // namespace + +Calls::Calls( + QWidget *parent, + not_null controller) +: Section(parent, controller) { + controller->session().api().authorizations().reload(); + + setupContent(); + requestPermissionAndStartTestingMicrophone(); +} + +Calls::~Calls() = default; + +rpl::producer Calls::title() { + return tr::lng_settings_section_devices(); +} + +Webrtc::VideoTrack *Calls::AddCameraSubsection( + std::shared_ptr show, + not_null content, + bool saveToSettings) { + auto &lifetime = content->lifetime(); + + const auto hasCall = (Core::App().calls().currentCall() != nullptr); + + auto capturerOwner = lifetime.make_state< + std::shared_ptr + >(); + + const auto track = lifetime.make_state( + (hasCall + ? VideoState::Inactive + : VideoState::Active)); + + const auto deviceId = lifetime.make_state>( + Core::App().settings().cameraDeviceId()); + auto resolvedId = rpl::deferred([=] { + return DeviceIdOrDefault(deviceId->value()); + }); + AddButtonWithLabel( + content, + tr::lng_settings_call_input_device(), + CameraDeviceNameValue(rpl::duplicate(resolvedId)), + st::settingsButtonNoIcon + )->addClickHandler([=] { + show->show(ChooseCameraDeviceBox( + rpl::duplicate(resolvedId), + [=](const QString &id) { + *deviceId = id; + if (saveToSettings) { + Core::App().settings().setCameraDeviceId(id); + Core::App().saveSettingsDelayed(); + } + if (*capturerOwner) { + (*capturerOwner)->switchToDevice( + id.toStdString(), + false); + } + })); + }); + const auto bubbleWrap = content->add(object_ptr(content)); + const auto bubble = lifetime.make_state<::Calls::VideoBubble>( + bubbleWrap, + track); + const auto padding = st::settingsButtonNoIcon.padding.left(); + const auto top = st::boxRoundShadow.extend.top(); + const auto bottom = st::boxRoundShadow.extend.bottom(); + + auto frameSize = track->renderNextFrame( + ) | rpl::map([=] { + return track->frameSize(); + }) | rpl::filter([=](QSize size) { + return !size.isEmpty() + && !Core::App().calls().currentCall() + && !Core::App().calls().currentGroupCall(); + }); + auto bubbleWidth = bubbleWrap->widthValue( + ) | rpl::filter([=](int width) { + return width > 2 * padding + 1; + }); + rpl::combine( + std::move(bubbleWidth), + std::move(frameSize) + ) | rpl::on_next([=](int width, QSize frame) { + const auto useWidth = (width - 2 * padding); + const auto useHeight = std::min( + ((useWidth * frame.height()) / frame.width()), + (useWidth * 480) / 640); + bubbleWrap->resize(width, top + useHeight + bottom); + bubble->updateGeometry( + ::Calls::VideoBubble::DragMode::None, + QRect(padding, top, useWidth, useHeight)); + bubbleWrap->update(); + }, bubbleWrap->lifetime()); + + using namespace rpl::mappers; + const auto checkCapturer = [=] { + if (*capturerOwner + || Core::App().calls().currentCall() + || Core::App().calls().currentGroupCall()) { + return; + } + *capturerOwner = Core::App().calls().getVideoCapture( + Core::App().settings().cameraDeviceId(), + false); + (*capturerOwner)->setPreferredAspectRatio(0.); + track->setState(VideoState::Active); + (*capturerOwner)->setState(tgcalls::VideoState::Active); + (*capturerOwner)->setOutput(track->sink()); + }; + rpl::combine( + Core::App().calls().currentCallValue(), + Core::App().calls().currentGroupCallValue(), + _1 || _2 + ) | rpl::on_next([=](bool has) { + if (has) { + track->setState(VideoState::Inactive); + bubbleWrap->resize(bubbleWrap->width(), 0); + *capturerOwner = nullptr; + } else { + crl::on_main(content, checkCapturer); + } + }, lifetime); + + return track; +} + +void Calls::sectionSaveChanges(FnMut done) { + _testingMicrophone = false; + done(); +} + +void Calls::setupContent() { + const auto content = Ui::CreateChild(this); + build(content, Builder::CallsSection); + Ui::ResizeFitChild(this, content); +} + +void Calls::requestPermissionAndStartTestingMicrophone() { + using PermissionType = ::Platform::PermissionType; + using PermissionStatus = ::Platform::PermissionStatus; + const auto status = GetPermissionStatus( + PermissionType::Microphone); + if (status == PermissionStatus::Granted) { + _testingMicrophone = true; + } else if (status == PermissionStatus::CanRequest) { + const auto startTestingChecked = crl::guard(this, [=]( + PermissionStatus status) { + if (status == PermissionStatus::Granted) { + crl::on_main(crl::guard(this, [=] { + _testingMicrophone = true; + })); + } + }); + RequestPermission( + PermissionType::Microphone, + startTestingChecked); + } else { + const auto showSystemSettings = [controller = controller()] { + OpenSystemSettingsForPermission( + PermissionType::Microphone); + controller->hideLayer(); + }; + controller()->show(Ui::MakeConfirmBox({ + .text = tr::lng_no_mic_permission(), + .confirmed = showSystemSettings, + .confirmText = tr::lng_menu_settings(), + })); + } +} + +rpl::producer PlaybackDeviceNameValue(rpl::producer id) { + return DeviceNameValue(DeviceType::Playback, std::move(id)); +} + +rpl::producer CaptureDeviceNameValue(rpl::producer id) { + return DeviceNameValue(DeviceType::Capture, std::move(id)); +} + +rpl::producer CameraDeviceNameValue( + rpl::producer id) { + return DeviceNameValue(DeviceType::Camera, std::move(id)); +} + object_ptr ChoosePlaybackDeviceBox( rpl::producer currentId, Fn chosen, @@ -687,5 +862,9 @@ object_ptr ChooseCameraDeviceBox( radioSt); } -} // namespace Settings +namespace Builder { +SectionBuildMethod CallsSection = kMeta.build; + +} // namespace Builder +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_calls.h b/Telegram/SourceFiles/settings/sections/settings_calls.h similarity index 81% rename from Telegram/SourceFiles/settings/settings_calls.h rename to Telegram/SourceFiles/settings/sections/settings_calls.h index 567d833b3d..2f71e07a29 100644 --- a/Telegram/SourceFiles/settings/settings_calls.h +++ b/Telegram/SourceFiles/settings/sections/settings_calls.h @@ -8,29 +8,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "settings/settings_common_session.h" -#include "ui/effects/animations.h" -#include "base/timer.h" +#include "settings/settings_type.h" namespace style { struct Checkbox; struct Radio; } // namespace style -namespace Calls { -class Call; -} // namespace Calls - -namespace Ui { -class LevelMeter; -class GenericBox; -class Show; -} // namespace Ui - namespace Webrtc { -class AudioInputTester; class VideoTrack; } // namespace Webrtc +namespace Ui { +class GenericBox; +class Show; +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + namespace Settings { class Calls : public Section { @@ -51,17 +49,6 @@ private: void setupContent(); void requestPermissionAndStartTestingMicrophone(); - void initPlaybackButton( - not_null container, - rpl::producer text, - rpl::producer resolvedId, - Fn set); - void initCaptureButton( - not_null container, - rpl::producer text, - rpl::producer resolvedId, - Fn set); - rpl::event_stream _cameraNameStream; rpl::variable _testingMicrophone; @@ -92,5 +79,9 @@ inline constexpr auto kMicTestAnimationDuration = crl::time(200); const style::Checkbox *st = nullptr, const style::Radio *radioSt = nullptr); -} // namespace Settings +namespace Builder { +extern SectionBuildMethod CallsSection; + +} // namespace Builder +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/sections/settings_main.cpp b/Telegram/SourceFiles/settings/sections/settings_main.cpp index 0e9ffbd151..47779475cf 100644 --- a/Telegram/SourceFiles/settings/sections/settings_main.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_main.cpp @@ -43,7 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/cloud_password/settings_cloud_password_input.h" #include "settings/sections/settings_advanced.h" #include "settings/settings_business.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" #include "settings/sections/settings_chat.h" #include "settings/settings_codes.h" #include "settings/settings_credits.h" diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index d0b99fbbcb..94789f6887 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -38,7 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_document.h" -#include "settings/settings_calls.h" // Calls::AddCameraSubsection. +#include "settings/sections/settings_calls.h" // Calls::AddCameraSubsection. #include "settings/settings_privacy_controllers.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 9b754f7865..006c8d01de 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -38,7 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "mtproto/mtproto_config.h" #include "settings/sections/settings_advanced.h" -#include "settings/settings_calls.h" +#include "settings/sections/settings_calls.h" #include "settings/settings_information.h" #include "storage/localstorage.h" #include "storage/storage_account.h"