diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d3fc4378f..0e4ddf251 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1308,6 +1308,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_migrate_learn_more" = "Learn more ยป"; "lng_profile_migrate_button" = "Upgrade to supergroup"; "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group."; +"lng_profile_camera_title" = "Capture yourself"; "lng_channel_not_accessible" = "Sorry, this channel is not accessible."; "lng_group_not_accessible" = "Sorry, this group is not accessible."; @@ -1459,6 +1460,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_attach_failed" = "Failed"; "lng_attach_file" = "File"; "lng_attach_photo" = "Photo"; +"lng_attach_camera" = "Camera"; "lng_media_open_with" = "Open With"; "lng_media_download" = "Download"; diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp index 49d48df3a..a77ecf902 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp @@ -67,7 +67,8 @@ void OpenWithPreparedFile( void PrepareProfilePhoto( not_null parent, not_null controller, - Fn &&doneCallback) { + Fn &&doneCallback, + QImage &&image) { const auto resizeToMinSize = [=]( QImage &&image, Qt::AspectRatioMode mode) { @@ -82,8 +83,53 @@ void PrepareProfilePhoto( return std::move(image); }; + if (image.isNull() + || (image.width() > (10 * image.height())) + || (image.height() > (10 * image.width()))) { + controller->show(Ui::MakeInformBox(tr::lng_bad_photo())); + return; + } + image = resizeToMinSize( + std::move(image), + Qt::KeepAspectRatioByExpanding); + const auto fileImage = std::make_shared(std::move(image)); + + auto applyModifications = [=, done = std::move(doneCallback)]( + const PhotoModifications &mods) { + done(resizeToMinSize( + ImageModified(fileImage->original(), mods), + Qt::KeepAspectRatio)); + }; + + auto crop = [&] { + const auto &i = fileImage; + const auto minSide = std::min(i->width(), i->height()); + return QRect( + (i->width() - minSide) / 2, + (i->height() - minSide) / 2, + minSide, + minSide); + }(); + + controller->showLayer( + std::make_unique( + parent, + controller, + fileImage, + PhotoModifications{ .crop = std::move(crop) }, + std::move(applyModifications), + EditorData{ + .cropType = EditorData::CropType::Ellipse, + .keepAspectRatio = true, }), + Ui::LayerOption::KeepOther); +} + +void PrepareProfilePhotoFromFile( + not_null parent, + not_null controller, + Fn &&doneCallback) { const auto callback = [=, done = std::move(doneCallback)]( - const FileDialog::OpenResult &result) { + const FileDialog::OpenResult &result) mutable { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { return; } @@ -93,45 +139,11 @@ void PrepareProfilePhoto( .content = result.remoteContent, .forceOpaque = true, }).image; - if (image.isNull() - || (image.width() > (10 * image.height())) - || (image.height() > (10 * image.width()))) { - controller->show(Ui::MakeInformBox(tr::lng_bad_photo())); - return; - } - image = resizeToMinSize( - std::move(image), - Qt::KeepAspectRatioByExpanding); - const auto fileImage = std::make_shared(std::move(image)); - - auto applyModifications = [=, done = std::move(done)]( - const PhotoModifications &mods) { - done(resizeToMinSize( - ImageModified(fileImage->original(), mods), - Qt::KeepAspectRatio)); - }; - - auto crop = [&] { - const auto &i = fileImage; - const auto minSide = std::min(i->width(), i->height()); - return QRect( - (i->width() - minSide) / 2, - (i->height() - minSide) / 2, - minSide, - minSide); - }(); - - controller->showLayer( - std::make_unique( - parent, - controller, - fileImage, - PhotoModifications{ .crop = std::move(crop) }, - std::move(applyModifications), - EditorData{ - .cropType = EditorData::CropType::Ellipse, - .keepAspectRatio = true, }), - Ui::LayerOption::KeepOther); + PrepareProfilePhoto( + parent, + controller, + std::move(done), + std::move(image)); }; FileDialog::GetOpenPath( parent.get(), diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h index 3a278afb5..44b226c9f 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h @@ -32,6 +32,12 @@ void OpenWithPreparedFile( Fn &&doneCallback); void PrepareProfilePhoto( + not_null parent, + not_null controller, + Fn &&doneCallback, + QImage &&image); + +void PrepareProfilePhotoFromFile( not_null parent, not_null controller, Fn &&doneCallback); diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 27a6e897d..4b39177b9 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -56,6 +56,155 @@ Calls::Calls( Calls::~Calls() = default; +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); + + const auto cameraNameStream = lifetime.make_state< + rpl::event_stream + >(); + + auto capturerOwner = lifetime.make_state< + std::shared_ptr + >(); + + const auto track = lifetime.make_state( + (hasCall + ? VideoState::Inactive + : VideoState::Active)); + + const auto currentCameraName = [&] { + const auto cameras = GetVideoInputList(); + const auto i = ranges::find( + cameras, + Core::App().settings().callVideoInputDeviceId(), + &VideoInput::id); + return (i != end(cameras)) + ? i->name + : tr::lng_settings_call_device_default(tr::now); + }(); + + AddButtonWithLabel( + content, + tr::lng_settings_call_input_device(), + rpl::single( + currentCameraName + ) | rpl::then( + cameraNameStream->events() + ), + st::settingsButtonNoIcon + )->addClickHandler([=] { + const auto &devices = GetVideoInputList(); + const auto options = ranges::views::concat( + ranges::views::single( + tr::lng_settings_call_device_default(tr::now)), + devices | ranges::views::transform(&VideoInput::name) + ) | ranges::to_vector; + const auto i = ranges::find( + devices, + Core::App().settings().callVideoInputDeviceId(), + &VideoInput::id); + const auto currentOption = (i != end(devices)) + ? int(i - begin(devices) + 1) + : 0; + const auto save = crl::guard(content, [=](int option) { + cameraNameStream->fire_copy(options[option]); + const auto deviceId = option + ? devices[option - 1].id + : "default"; + if (saveToSettings) { + Core::App().settings().setCallVideoInputDeviceId(deviceId); + Core::App().saveSettingsDelayed(); + } + if (const auto call = Core::App().calls().currentCall()) { + call->setCurrentCameraDevice(deviceId); + } + if (*capturerOwner) { + (*capturerOwner)->switchToDevice( + deviceId.toStdString(), + false); + } + }); + show->showBox(Box([=](not_null box) { + SingleChoiceBox(box, { + .title = tr::lng_settings_call_camera(), + .options = options, + .initialSelection = currentOption, + .callback = save, + }); + })); + }); + 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::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().callVideoInputDeviceId(), + 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 done) { if (_micTester) { _micTester.reset(); @@ -66,144 +215,13 @@ void Calls::sectionSaveChanges(FnMut done) { void Calls::setupContent() { const auto content = Ui::CreateChild(this); - const auto &settings = Core::App().settings(); - const auto cameras = GetVideoInputList(); - if (!cameras.empty()) { - const auto hasCall = (Core::App().calls().currentCall() != nullptr); - - auto capturerOwner = content->lifetime().make_state< - std::shared_ptr - >(); - - const auto track = content->lifetime().make_state( - (hasCall - ? VideoState::Inactive - : VideoState::Active)); - - const auto currentCameraName = [&] { - const auto i = ranges::find( - cameras, - settings.callVideoInputDeviceId(), - &VideoInput::id); - return (i != end(cameras)) - ? i->name - : tr::lng_settings_call_device_default(tr::now); - }(); - + if (!GetVideoInputList().empty()) { AddSkip(content); AddSubsectionTitle(content, tr::lng_settings_call_camera()); - AddButtonWithLabel( + AddCameraSubsection( + std::make_shared(_controller), content, - tr::lng_settings_call_input_device(), - rpl::single( - currentCameraName - ) | rpl::then( - _cameraNameStream.events() - ), - st::settingsButtonNoIcon - )->addClickHandler([=] { - const auto &devices = GetVideoInputList(); - const auto options = ranges::views::concat( - ranges::views::single( - tr::lng_settings_call_device_default(tr::now)), - devices | ranges::views::transform(&VideoInput::name) - ) | ranges::to_vector; - const auto i = ranges::find( - devices, - Core::App().settings().callVideoInputDeviceId(), - &VideoInput::id); - const auto currentOption = (i != end(devices)) - ? int(i - begin(devices) + 1) - : 0; - const auto save = crl::guard(this, [=](int option) { - _cameraNameStream.fire_copy(options[option]); - const auto deviceId = option - ? devices[option - 1].id - : "default"; - Core::App().settings().setCallVideoInputDeviceId(deviceId); - Core::App().saveSettingsDelayed(); - if (const auto call = Core::App().calls().currentCall()) { - call->setCurrentCameraDevice(deviceId); - } - if (*capturerOwner) { - (*capturerOwner)->switchToDevice( - deviceId.toStdString(), - false); - } - }); - _controller->show(Box([=](not_null box) { - SingleChoiceBox(box, { - .title = tr::lng_settings_call_camera(), - .options = options, - .initialSelection = currentOption, - .callback = save, - }); - })); - }); - const auto bubbleWrap = content->add(object_ptr(content)); - const auto bubble = content->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().callVideoInputDeviceId(), - 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); - } - }, content->lifetime()); - + true); AddSkip(content); AddDivider(content); } diff --git a/Telegram/SourceFiles/settings/settings_calls.h b/Telegram/SourceFiles/settings/settings_calls.h index 679b167fa..dbc6af95c 100644 --- a/Telegram/SourceFiles/settings/settings_calls.h +++ b/Telegram/SourceFiles/settings/settings_calls.h @@ -23,10 +23,12 @@ class Call; namespace Ui { class LevelMeter; class GenericBox; +class Show; } // namespace Ui namespace Webrtc { class AudioInputTester; +class VideoTrack; } // namespace Webrtc namespace Settings { @@ -38,6 +40,11 @@ public: void sectionSaveChanges(FnMut done) override; + static Webrtc::VideoTrack *AddCameraSubsection( + std::shared_ptr show, + not_null content, + bool saveToSettings); + private: void setupContent(); void requestPermissionAndStartTestingMicrophone(); diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 5f39d2981..b43fbd52b 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -66,7 +66,7 @@ void SetupPhoto( auto callback = [=](QImage &&image) { self->session().api().peerPhoto().upload(self, std::move(image)); }; - Editor::PrepareProfilePhoto( + Editor::PrepareProfilePhotoFromFile( upload, &controller->window(), std::move(callback)); diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp index 6e7491cf2..b47d48534 100644 --- a/Telegram/SourceFiles/ui/special_buttons.cpp +++ b/Telegram/SourceFiles/ui/special_buttons.cpp @@ -7,8 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/special_buttons.h" -#include "styles/style_boxes.h" -#include "styles/style_chat.h" +#include "base/call_delayed.h" #include "dialogs/ui/dialogs_layout.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/radial_animation.h" @@ -32,19 +31,66 @@ 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 "calls/calls_instance.h" +#include "webrtc/webrtc_media_devices.h" // Webrtc::GetVideoInputList. +#include "webrtc/webrtc_video_track.h" +#include "ui/widgets/popup_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "apiwrap.h" -#include "mainwidget.h" -#include "facades.h" +#include "styles/style_boxes.h" +#include "styles/style_chat.h" namespace Ui { namespace { constexpr auto kAnimationDuration = crl::time(120); +bool IsCameraAvailable() { + return (Core::App().calls().currentCall() == nullptr) + && !Webrtc::GetVideoInputList().empty(); +} + +void CameraBox( + not_null box, + not_null controller, + Fn &&doneCallback) { + using namespace Webrtc; + + const auto track = Settings::Calls::AddCameraSubsection( + std::make_shared(box), + box->verticalLayout(), + false); + if (!track) { + box->closeBox(); + return; + } + track->stateValue( + ) | rpl::start_with_next([=](const VideoState &state) { + if (state == VideoState::Inactive) { + box->closeBox(); + } + }, box->lifetime()); + + auto done = [=, done = std::move(doneCallback)](QImage &&image) { + box->closeBox(); + done(std::move(image)); + }; + + box->setTitle(tr::lng_profile_camera_title()); + box->addButton(tr::lng_continue(), [=, done = std::move(done)]() mutable { + Editor::PrepareProfilePhoto( + box, + controller, + std::move(done), + track->frame(FrameRequest()).mirrored(true, false)); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + QString CropTitle(not_null peer) { if (peer->isChat() || peer->isMegagroup()) { return tr::lng_create_group_crop(tr::now); @@ -198,10 +244,7 @@ void UserpicButton::prepare() { void UserpicButton::setClickHandlerByRole() { switch (_role) { case Role::ChangePhoto: - addClickHandler(App::LambdaDelayed( - _st.changeButton.ripple.hideDuration, - this, - [=] { changePhotoLocally(); })); + addClickHandler([=] { changePhotoLocally(); }); break; case Role::OpenPhoto: @@ -230,10 +273,26 @@ void UserpicButton::changePhotoLocally(bool requestToUpload) { _uploadPhotoRequests.fire({}); } }; - Editor::PrepareProfilePhoto( - this, - _window, - std::move(callback)); + const auto chooseFile = [=] { + base::call_delayed( + _st.changeButton.ripple.hideDuration, + crl::guard(this, [=] { + Editor::PrepareProfilePhotoFromFile( + this, + _window, + callback); + })); + }; + if (!IsCameraAvailable()) { + chooseFile(); + } else { + _menu = base::make_unique_q(this); + _menu->addAction(tr::lng_attach_file(tr::now), chooseFile); + _menu->addAction(tr::lng_attach_camera(tr::now), [=] { + _window->show(Box(CameraBox, _window, callback)); + }); + _menu->popup(QCursor::pos()); + } } void UserpicButton::openPeerPhoto() { diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h index 38afa112e..56cddef54 100644 --- a/Telegram/SourceFiles/ui/special_buttons.h +++ b/Telegram/SourceFiles/ui/special_buttons.h @@ -36,6 +36,8 @@ struct Information; namespace Ui { +class PopupMenu; + class HistoryDownButton : public RippleButton { public: HistoryDownButton(QWidget *parent, const style::TwoIconButton &st); @@ -158,6 +160,8 @@ private: std::unique_ptr _streamed; PhotoData *_streamedPhoto = nullptr; + base::unique_qptr _menu; + bool _showSavedMessagesOnSelf = false; bool _canOpenPhoto = false; bool _cursorInChangeOverlay = false;