691 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			691 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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 "settings/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 "calls/calls_call.h"
 | |
| #include "calls/calls_instance.h"
 | |
| #include "calls/calls_video_bubble.h"
 | |
| #include "apiwrap.h"
 | |
| #include "api/api_authorizations.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 "styles/style_layers.h"
 | |
| 
 | |
| namespace Settings {
 | |
| namespace {
 | |
| 
 | |
| using namespace Webrtc;
 | |
| 
 | |
| [[nodiscard]] rpl::producer<QString> DeviceNameValue(
 | |
| 		DeviceType type,
 | |
| 		rpl::producer<QString> id) {
 | |
| 	return std::move(id) | rpl::map([type](const QString &id) {
 | |
| 		return Core::App().mediaDevices().devicesValue(
 | |
| 			type
 | |
| 		) | rpl::map([id](const std::vector<DeviceInfo> &list) {
 | |
| 			const auto i = ranges::find(list, id, &DeviceInfo::id);
 | |
| 			return (i != end(list) && !i->inactive)
 | |
| 				? i->name
 | |
| 				: tr::lng_settings_call_device_default(tr::now);
 | |
| 		});
 | |
| 	}) | rpl::flatten_latest();
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| Calls::Calls(
 | |
| 	QWidget *parent,
 | |
| 	not_null<Window::SessionController*> controller)
 | |
| : Section(parent)
 | |
| , _controller(controller) {
 | |
| 	// Request valid value of calls disabled flag.
 | |
| 	controller->session().api().authorizations().reload();
 | |
| 
 | |
| 	setupContent();
 | |
| 	requestPermissionAndStartTestingMicrophone();
 | |
| }
 | |
| 
 | |
| Calls::~Calls() = default;
 | |
| 
 | |
| rpl::producer<QString> Calls::title() {
 | |
| 	return tr::lng_settings_section_devices();
 | |
| }
 | |
| 
 | |
| Webrtc::VideoTrack *Calls::AddCameraSubsection(
 | |
| 		std::shared_ptr<Ui::Show> show,
 | |
| 		not_null<Ui::VerticalLayout*> content,
 | |
| 		bool saveToSettings) {
 | |
| 	auto &lifetime = content->lifetime();
 | |
| 
 | |
| 	const auto hasCall = (Core::App().calls().currentCall() != nullptr);
 | |
| 
 | |
| 	auto capturerOwner = lifetime.make_state<
 | |
| 		std::shared_ptr<tgcalls::VideoCaptureInterface>
 | |
| 	>();
 | |
| 
 | |
| 	const auto track = lifetime.make_state<VideoTrack>(
 | |
| 		(hasCall
 | |
| 			? VideoState::Inactive
 | |
| 			: VideoState::Active));
 | |
| 
 | |
| 	const auto deviceId = lifetime.make_state<rpl::variable<QString>>(
 | |
| 		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<Ui::RpWidget>(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::start_with_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::start_with_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<void()> done) {
 | |
| 	_testingMicrophone = false;
 | |
| 	done();
 | |
| }
 | |
| 
 | |
| void Calls::setupContent() {
 | |
| 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(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<Ui::SettingsButton>(
 | |
| 		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::start_with_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<Ui::SlideWrap<Ui::VerticalLayout>>(
 | |
| 			content,
 | |
| 			object_ptr<Ui::VerticalLayout>(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<Ui::SettingsButton>(
 | |
| 		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());
 | |
| 	}) | start_with_next([=](bool value) {
 | |
| 		api->authorizations().toggleCallsDisabledHere(!value);
 | |
| 	}, content->lifetime());
 | |
| 
 | |
| 	content->add(object_ptr<Ui::SettingsButton>(
 | |
| 		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(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		rpl::producer<QString> text,
 | |
| 		rpl::producer<QString> resolvedId,
 | |
| 		Fn<void(QString)> set) {
 | |
| 	AddButtonWithLabel(
 | |
| 		container,
 | |
| 		tr::lng_settings_call_output_device(),
 | |
| 		PlaybackDeviceNameValue(rpl::duplicate(resolvedId)),
 | |
| 		st::settingsButtonNoIcon
 | |
| 	)->addClickHandler([=] {
 | |
| 		_controller->show(ChoosePlaybackDeviceBox(
 | |
| 			rpl::duplicate(resolvedId),
 | |
| 			[=](const QString &id) {
 | |
| 				set(id);
 | |
| 				Core::App().saveSettingsDelayed();
 | |
| 			}));
 | |
| 	});
 | |
| 
 | |
| }
 | |
| 
 | |
| void Calls::initCaptureButton(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		rpl::producer<QString> text,
 | |
| 		rpl::producer<QString> resolvedId,
 | |
| 		Fn<void(QString)> set) {
 | |
| 	AddButtonWithLabel(
 | |
| 		container,
 | |
| 		tr::lng_settings_call_input_device(),
 | |
| 		CaptureDeviceNameValue(rpl::duplicate(resolvedId)),
 | |
| 		st::settingsButtonNoIcon
 | |
| 	)->addClickHandler([=] {
 | |
| 		_controller->show(ChooseCaptureDeviceBox(
 | |
| 			rpl::duplicate(resolvedId),
 | |
| 			[=](const QString &id) {
 | |
| 				set(id);
 | |
| 				Core::App().saveSettingsDelayed();
 | |
| 			}));
 | |
| 	});
 | |
| 
 | |
| 	struct LevelState {
 | |
| 		std::unique_ptr<Webrtc::DeviceResolver> deviceId;
 | |
| 		std::unique_ptr<Webrtc::AudioInputTester> tester;
 | |
| 		base::Timer timer;
 | |
| 		Ui::Animations::Simple animation;
 | |
| 		float level = 0.;
 | |
| 	};
 | |
| 	const auto level = container->add(
 | |
| 		object_ptr<Ui::LevelMeter>(
 | |
| 			container,
 | |
| 			st::defaultLevelMeter),
 | |
| 		st::settingsLevelMeterPadding);
 | |
| 	const auto state = level->lifetime().make_state<LevelState>();
 | |
| 	level->resize(QSize(0, st::defaultLevelMeter.height));
 | |
| 
 | |
| 	state->timer.setCallback([=] {
 | |
| 		const auto was = state->level;
 | |
| 		state->level = state->tester->getAndResetLevel();
 | |
| 		state->animation.start([=] {
 | |
| 			level->setValue(state->animation.value(state->level));
 | |
| 		}, was, state->level, kMicTestAnimationDuration);
 | |
| 	});
 | |
| 	_testingMicrophone.value() | rpl::start_with_next([=](bool testing) {
 | |
| 		if (testing) {
 | |
| 			state->deviceId = std::make_unique<Webrtc::DeviceResolver>(
 | |
| 				&Core::App().mediaDevices(),
 | |
| 				Webrtc::DeviceType::Capture,
 | |
| 				rpl::duplicate(resolvedId));
 | |
| 			state->tester = std::make_unique<AudioInputTester>(
 | |
| 				state->deviceId->value());
 | |
| 			state->timer.callEach(kMicTestUpdateInterval);
 | |
| 		} else {
 | |
| 			state->timer.cancel();
 | |
| 			state->animation.stop();
 | |
| 			state->tester = nullptr;
 | |
| 			state->deviceId = nullptr;
 | |
| 		}
 | |
| 	}, level->lifetime());
 | |
| }
 | |
| 
 | |
| void Calls::requestPermissionAndStartTestingMicrophone() {
 | |
| 	using namespace ::Platform;
 | |
| 	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<QString> PlaybackDeviceNameValue(rpl::producer<QString> id) {
 | |
| 	return DeviceNameValue(DeviceType::Playback, std::move(id));
 | |
| }
 | |
| 
 | |
| rpl::producer<QString> CaptureDeviceNameValue(rpl::producer<QString> id) {
 | |
| 	return DeviceNameValue(DeviceType::Capture, std::move(id));
 | |
| }
 | |
| 
 | |
| rpl::producer<QString> CameraDeviceNameValue(
 | |
| 		rpl::producer<QString> id) {
 | |
| 	return DeviceNameValue(DeviceType::Camera, std::move(id));
 | |
| }
 | |
| 
 | |
| void ChooseMediaDeviceBox(
 | |
| 		not_null<Ui::GenericBox*> box,
 | |
| 		rpl::producer<QString> title,
 | |
| 		rpl::producer<std::vector<DeviceInfo>> devicesValue,
 | |
| 		rpl::producer<QString> currentId,
 | |
| 		Fn<void(QString id)> chosen,
 | |
| 		const style::Checkbox *st,
 | |
| 		const style::Radio *radioSt) {
 | |
| 	box->setTitle(std::move(title));
 | |
| 	box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
 | |
| 	const auto layout = box->verticalLayout();
 | |
| 	const auto skip = st::boxOptionListPadding.top()
 | |
| 		+ st::defaultBoxCheckbox.margin.top();
 | |
| 	layout->add(object_ptr<Ui::FixedHeightWidget>(layout, skip));
 | |
| 
 | |
| 	if (!st) {
 | |
| 		st = &st::defaultBoxCheckbox;
 | |
| 	}
 | |
| 	if (!radioSt) {
 | |
| 		radioSt = &st::defaultRadio;
 | |
| 	}
 | |
| 
 | |
| 	struct State {
 | |
| 		std::vector<DeviceInfo> list;
 | |
| 		base::flat_map<int, QString> ids;
 | |
| 		rpl::variable<QString> currentId;
 | |
| 		QString currentName;
 | |
| 		bool ignoreValueChange = false;
 | |
| 	};
 | |
| 	const auto state = box->lifetime().make_state<State>();
 | |
| 	state->currentId = std::move(currentId);
 | |
| 
 | |
| 	const auto choose = [=](const QString &id) {
 | |
| 		const auto weak = Ui::MakeWeak(box);
 | |
| 		chosen(id);
 | |
| 		if (weak) {
 | |
| 			box->closeBox();
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	const auto group = std::make_shared<Ui::RadiobuttonGroup>();
 | |
| 	const auto fake = std::make_shared<Ui::RadiobuttonGroup>(0);
 | |
| 	const auto buttons = layout->add(object_ptr<Ui::VerticalLayout>(layout));
 | |
| 	const auto other = layout->add(object_ptr<Ui::VerticalLayout>(layout));
 | |
| 	const auto margins = QMargins(
 | |
| 		st::boxPadding.left() + st::boxOptionListPadding.left(),
 | |
| 		0,
 | |
| 		st::boxPadding.right(),
 | |
| 		st::boxOptionListSkip);
 | |
| 	const auto def = buttons->add(
 | |
| 		object_ptr<Ui::Radiobutton>(
 | |
| 			buttons,
 | |
| 			group,
 | |
| 			0,
 | |
| 			tr::lng_settings_call_device_default(tr::now),
 | |
| 			*st,
 | |
| 			*radioSt),
 | |
| 		margins);
 | |
| 	def->clicks(
 | |
| 	) | rpl::filter([=] {
 | |
| 		return !group->value();
 | |
| 	}) | rpl::start_with_next([=] {
 | |
| 		choose(kDefaultDeviceId);
 | |
| 	}, def->lifetime());
 | |
| 	const auto showUnavailable = [=](QString text) {
 | |
| 		AddSkip(other);
 | |
| 		AddSubsectionTitle(other, tr::lng_settings_devices_inactive());
 | |
| 		const auto &radio = *radioSt;
 | |
| 		const auto button = other->add(
 | |
| 			object_ptr<Ui::Radiobutton>(other, fake, 0, text, *st, radio),
 | |
| 			margins);
 | |
| 		button->show();
 | |
| 
 | |
| 		button->setDisabled(true);
 | |
| 		button->finishAnimating();
 | |
| 		button->setAttribute(Qt::WA_TransparentForMouseEvents);
 | |
| 		while (other->count() > 3) {
 | |
| 			delete other->widgetAt(0);
 | |
| 		}
 | |
| 		if (const auto width = box->width()) {
 | |
| 			other->resizeToWidth(width);
 | |
| 		}
 | |
| 	};
 | |
| 	const auto hideUnavailable = [=] {
 | |
| 		while (other->count() > 0) {
 | |
| 			delete other->widgetAt(0);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	const auto selectCurrent = [=](QString current) {
 | |
| 		state->ignoreValueChange = true;
 | |
| 		const auto guard = gsl::finally([&] {
 | |
| 			state->ignoreValueChange = false;
 | |
| 		});
 | |
| 		if (current.isEmpty() || current == kDefaultDeviceId) {
 | |
| 			group->setValue(0);
 | |
| 			hideUnavailable();
 | |
| 		} else {
 | |
| 			auto found = false;
 | |
| 			for (const auto &[index, id] : state->ids) {
 | |
| 				if (id == current) {
 | |
| 					group->setValue(index);
 | |
| 					found = true;
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 			if (found) {
 | |
| 				hideUnavailable();
 | |
| 			} else {
 | |
| 				group->setValue(0);
 | |
| 				const auto i = ranges::find(
 | |
| 					state->list,
 | |
| 					current,
 | |
| 					&DeviceInfo::id);
 | |
| 				if (i != end(state->list)) {
 | |
| 					showUnavailable(i->name);
 | |
| 				} else {
 | |
| 					hideUnavailable();
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	std::move(
 | |
| 		devicesValue
 | |
| 	) | rpl::start_with_next([=](std::vector<DeviceInfo> &&list) {
 | |
| 		auto count = buttons->count();
 | |
| 		auto index = 1;
 | |
| 		state->ids.clear();
 | |
| 		state->list = std::move(list);
 | |
| 
 | |
| 		state->ignoreValueChange = true;
 | |
| 		const auto guard = gsl::finally([&] {
 | |
| 			state->ignoreValueChange = false;
 | |
| 		});
 | |
| 
 | |
| 		const auto current = state->currentId.current();
 | |
| 		for (const auto &info : state->list) {
 | |
| 			const auto id = info.id;
 | |
| 			if (info.inactive) {
 | |
| 				continue;
 | |
| 			} else if (current == id) {
 | |
| 				group->setValue(index);
 | |
| 			}
 | |
| 			const auto button = buttons->insert(
 | |
| 				index,
 | |
| 				object_ptr<Ui::Radiobutton>(
 | |
| 					buttons,
 | |
| 					group,
 | |
| 					index,
 | |
| 					info.name,
 | |
| 					*st,
 | |
| 					*radioSt),
 | |
| 				margins);
 | |
| 			button->show();
 | |
| 			button->finishAnimating();
 | |
| 			button->clicks(
 | |
| 			) | rpl::filter([=] {
 | |
| 				return (group->current() == index);
 | |
| 			}) | rpl::start_with_next([=] {
 | |
| 				choose(id);
 | |
| 			}, button->lifetime());
 | |
| 
 | |
| 			state->ids.emplace(index, id);
 | |
| 			if (index < count) {
 | |
| 				delete buttons->widgetAt(index + 1);
 | |
| 			}
 | |
| 			++index;
 | |
| 		}
 | |
| 		while (index < count) {
 | |
| 			delete buttons->widgetAt(index);
 | |
| 			--count;
 | |
| 		}
 | |
| 		if (const auto width = box->width()) {
 | |
| 			buttons->resizeToWidth(width);
 | |
| 		}
 | |
| 		selectCurrent(current);
 | |
| 	}, box->lifetime());
 | |
| 
 | |
| 	state->currentId.changes(
 | |
| 	) | rpl::start_with_next(selectCurrent, box->lifetime());
 | |
| 
 | |
| 	def->finishAnimating();
 | |
| 
 | |
| 	group->setChangedCallback([=](int value) {
 | |
| 		if (state->ignoreValueChange) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto i = state->ids.find(value);
 | |
| 		choose((i != end(state->ids)) ? i->second : kDefaultDeviceId);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| object_ptr<Ui::GenericBox> ChoosePlaybackDeviceBox(
 | |
| 		rpl::producer<QString> currentId,
 | |
| 		Fn<void(QString id)> chosen,
 | |
| 		const style::Checkbox *st,
 | |
| 		const style::Radio *radioSt) {
 | |
| 	return Box(
 | |
| 		ChooseMediaDeviceBox,
 | |
| 		tr::lng_settings_call_output_device(),
 | |
| 		Core::App().mediaDevices().devicesValue(DeviceType::Playback),
 | |
| 		std::move(currentId),
 | |
| 		std::move(chosen),
 | |
| 		st,
 | |
| 		radioSt);
 | |
| }
 | |
| 
 | |
| object_ptr<Ui::GenericBox> ChooseCaptureDeviceBox(
 | |
| 		rpl::producer<QString> currentId,
 | |
| 		Fn<void(QString id)> chosen,
 | |
| 		const style::Checkbox *st,
 | |
| 		const style::Radio *radioSt) {
 | |
| 	return Box(
 | |
| 		ChooseMediaDeviceBox,
 | |
| 		tr::lng_settings_call_input_device(),
 | |
| 		Core::App().mediaDevices().devicesValue(DeviceType::Capture),
 | |
| 		std::move(currentId),
 | |
| 		std::move(chosen),
 | |
| 		st,
 | |
| 		radioSt);
 | |
| }
 | |
| 
 | |
| object_ptr<Ui::GenericBox> ChooseCameraDeviceBox(
 | |
| 		rpl::producer<QString> currentId,
 | |
| 		Fn<void(QString id)> chosen,
 | |
| 		const style::Checkbox *st,
 | |
| 		const style::Radio *radioSt) {
 | |
| 	return Box(
 | |
| 		ChooseMediaDeviceBox,
 | |
| 		tr::lng_settings_call_device_default(),
 | |
| 		Core::App().mediaDevices().devicesValue(DeviceType::Camera),
 | |
| 		std::move(currentId),
 | |
| 		std::move(chosen),
 | |
| 		st,
 | |
| 		radioSt);
 | |
| }
 | |
| 
 | |
| } // namespace Settings
 | |
| 
 | 
