From 112dea85941ccf7043914c67b30e5f5312558c5e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 2 Oct 2020 21:14:41 +0300 Subject: [PATCH] Created voice record bar as separated history view class. --- Telegram/CMakeLists.txt | 2 + .../history_view_voice_record_bar.cpp | 279 ++++++++++++++++++ .../controls/history_view_voice_record_bar.h | 93 ++++++ Telegram/SourceFiles/ui/chat/chat.style | 1 + 4 files changed, 375 insertions(+) create mode 100644 Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp create mode 100644 Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 85aed68e25..e686508b32 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -477,6 +477,8 @@ PRIVATE history/view/controls/compose_controls_common.h history/view/controls/history_view_compose_controls.cpp history/view/controls/history_view_compose_controls.h + history/view/controls/history_view_voice_record_bar.cpp + history/view/controls/history_view_voice_record_bar.h history/view/media/history_view_call.h history/view/media/history_view_call.cpp history/view/media/history_view_contact.h diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp new file mode 100644 index 0000000000..9685f5d019 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -0,0 +1,279 @@ +/* +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 "history/view/controls/history_view_voice_record_bar.h" + +#include "api/api_send_progress.h" +#include "core/application.h" +#include "lang/lang_keys.h" +#include "mainwindow.h" +#include "media/audio/media_audio.h" +#include "media/audio/media_audio_capture.h" +#include "styles/style_chat.h" +#include "ui/controls/send_button.h" +#include "ui/text/format_values.h" +#include "window/window_session_controller.h" + +namespace HistoryView::Controls { + +namespace { + +using SendActionUpdate = VoiceRecordBar::SendActionUpdate; +using VoiceToSend = VoiceRecordBar::VoiceToSend; + +constexpr auto kRecordingUpdateDelta = crl::time(100); +constexpr auto kMaxSamples = + ::Media::Player::kDefaultFrequency * AudioVoiceMsgMaxLength; + +[[nodiscard]] auto Duration(int samples) { + return samples / ::Media::Player::kDefaultFrequency; +} + +} // namespace + +VoiceRecordBar::VoiceRecordBar( + not_null parent, + not_null controller, + std::shared_ptr send, + int recorderHeight) +: RpWidget(parent) +, _controller(controller) +, _wrap(std::make_unique(parent)) +, _send(send) +, _cancelFont(st::historyRecordFont) +, _recordCancelWidth(_cancelFont->width(tr::lng_record_cancel(tr::now))) +, _recordingAnimation([=](crl::time now) { + return recordingAnimationCallback(now); +}) { + resize(QSize(parent->width(), recorderHeight)); + init(); +} + +VoiceRecordBar::~VoiceRecordBar() { + if (isRecording()) { + stopRecording(false); + } +} + +void VoiceRecordBar::updateControlsGeometry(QSize size) { + _centerY = size.height() / 2; + { + const auto maxD = st::historyRecordSignalMax * 2; + const auto point = _centerY - st::historyRecordSignalMax; + _redCircleRect = { point, point, maxD, maxD }; + } +} + +void VoiceRecordBar::init() { + hide(); + // Keep VoiceRecordBar behind SendButton. + rpl::single( + ) | rpl::then( + _send->events( + ) | rpl::filter([](not_null e) { + return e->type() == QEvent::ZOrderChange; + }) | rpl::to_empty + ) | rpl::start_with_next([=] { + stackUnder(_send.get()); + }, lifetime()); + + sizeValue( + ) | rpl::start_with_next([=](QSize size) { + updateControlsGeometry(size); + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + p.fillRect(rect(), st::historyComposeAreaBg); + + drawRecording(p, _send->recordActiveRatio()); + }, lifetime()); +} + +void VoiceRecordBar::startRecording() { + using namespace ::Media::Capture; + if (!instance()->available()) { + return; + } + show(); + _recording = true; + + instance()->start(); + instance()->updated( + ) | rpl::start_with_next_error([=](const Update &update) { + recordUpdated(update.level, update.samples); + }, [=] { + stopRecording(false); + }, _recordingLifetime); + + _inField = true; + _controller->widget()->setInnerFocus(); + + update(); + _send->setRecordActive(true); + + _send->events( + ) | rpl::filter([=](not_null e) { + return isTypeRecord() + && (e->type() == QEvent::MouseMove + || e->type() == QEvent::MouseButtonRelease); + }) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::MouseMove) { + const auto mouse = static_cast(e.get()); + const auto pos = mapFromGlobal(mouse->globalPos()); + const auto inField = rect().contains(pos); + if (inField != _inField) { + _inField = inField; + _send->setRecordActive(_inField); + } + } else if (type == QEvent::MouseButtonRelease) { + stopRecording(_inField); + } + }, _recordingLifetime); +} + +bool VoiceRecordBar::recordingAnimationCallback(crl::time now) { + const auto dt = anim::Disabled() + ? 1. + : ((now - _recordingAnimation.started()) + / float64(kRecordingUpdateDelta)); + if (dt >= 1.) { + _recordingLevel.finish(); + } else { + _recordingLevel.update(dt, anim::linear); + } + if (!anim::Disabled()) { + update(_redCircleRect); + } + return (dt < 1.); +} + +void VoiceRecordBar::recordUpdated(quint16 level, int samples) { + _recordingLevel.start(level); + _recordingAnimation.start(); + _recordingSamples = samples; + if (samples < 0 || samples >= kMaxSamples) { + stopRecording(samples > 0 && _inField); + } + Core::App().updateNonIdle(); + update(); + _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice }); +} + +void VoiceRecordBar::stopRecording(bool send) { + hide(); + _recording = false; + + using namespace ::Media::Capture; + if (send) { + instance()->stop(crl::guard(this, [=](const Result &data) { + if (data.bytes.isEmpty()) { + return; + } + + Window::ActivateWindow(_controller); + const auto duration = Duration(data.samples); + _sendVoiceRequests.fire({ data.bytes, data.waveform, duration }); + })); + } else { + instance()->stop(); + } + + _recordingLevel = anim::value(); + _recordingAnimation.stop(); + + _recordingLifetime.destroy(); + _recordingSamples = 0; + _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); + + _controller->widget()->setInnerFocus(); + + update(); + _send->setRecordActive(false); +} + +void VoiceRecordBar::drawRecording(Painter &p, float64 recordActive) { + p.setPen(Qt::NoPen); + p.setBrush(st::historyRecordSignalColor); + + { + PainterHighQualityEnabler hq(p); + const auto min = st::historyRecordSignalMin; + const auto max = st::historyRecordSignalMax; + const auto delta = std::min(_recordingLevel.current() / 0x4000, 1.); + const auto radii = qRound(min + (delta * (max - min))); + const auto center = _redCircleRect.center() + QPoint(1, 1); + p.drawEllipse(center, radii, radii); + } + + const auto duration = Ui::FormatDurationText(Duration(_recordingSamples)); + p.setFont(_cancelFont); + p.setPen(st::historyRecordDurationFg); + + const auto durationLeft = _redCircleRect.x() + + _redCircleRect.width() + + st::historyRecordDurationSkip; + const auto durationWidth = _cancelFont->width(duration); + p.drawText( + QRect( + durationLeft, + _redCircleRect.y(), + durationWidth, + _redCircleRect.height()), + style::al_left, + duration); + + const auto leftCancel = durationLeft + + durationWidth + + ((_send->width() - st::historyRecordVoice.width()) / 2); + const auto rightCancel = width() - _send->width(); + + p.setPen( + anim::pen( + st::historyRecordCancel, + st::historyRecordCancelActive, + 1. - recordActive)); + p.drawText( + leftCancel + (rightCancel - leftCancel - _recordCancelWidth) / 2, + st::historyRecordTextTop + _cancelFont->ascent, + tr::lng_record_cancel(tr::now)); +} + +rpl::producer VoiceRecordBar::sendActionUpdates() const { + return _sendActionUpdates.events(); +} + +rpl::producer VoiceRecordBar::sendVoiceRequests() const { + return _sendVoiceRequests.events(); +} + +bool VoiceRecordBar::isRecording() const { + return _recording.current(); +} + +void VoiceRecordBar::finishAnimating() { + _recordingAnimation.stop(); +} + +rpl::producer VoiceRecordBar::recordingStateChanges() const { + return _recording.changes(); +} + +rpl::producer<> VoiceRecordBar::startRecordingRequests() const { + return _send->events( + ) | rpl::filter([=](not_null e) { + return isTypeRecord() && (e->type() == QEvent::MouseButtonPress); + }) | rpl::to_empty; +} + +bool VoiceRecordBar::isTypeRecord() const { + return (_send->type() == Ui::SendButton::Type::Record); +} + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h new file mode 100644 index 0000000000..3e2c8ce5d0 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -0,0 +1,93 @@ +/* +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 "api/api_common.h" +#include "history/view/controls/compose_controls_common.h" +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" + +namespace Ui { +class SendButton; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView::Controls { + +class VoiceRecordBar final : public Ui::RpWidget { +public: + using SendActionUpdate = Controls::SendActionUpdate; + using VoiceToSend = Controls::VoiceToSend; + + void startRecording(); + void finishAnimating(); + + [[nodiscard]] rpl::producer sendActionUpdates() const; + [[nodiscard]] rpl::producer sendVoiceRequests() const; + [[nodiscard]] rpl::producer recordingStateChanges() const; + [[nodiscard]] rpl::producer<> startRecordingRequests() const; + + [[nodiscard]] bool isRecording() const; + + VoiceRecordBar( + not_null parent, + not_null controller, + std::shared_ptr send, + int recorderHeight); + ~VoiceRecordBar(); + +private: + void init(); + + void updateControlsGeometry(QSize size); + + void recordError(); + void recordUpdated(quint16 level, int samples); + + bool recordingAnimationCallback(crl::time now); + void stopRecording(bool send); + + void recordStopCallback(bool active); + void recordUpdateCallback(QPoint globalPos); + + bool showRecordButton() const; + void drawRecording(Painter &p, float64 recordActive); + void updateOverStates(QPoint pos); + + bool isTypeRecord() const; + + const not_null _controller; + const std::unique_ptr _wrap; + const std::shared_ptr _send; + + rpl::event_stream _sendActionUpdates; + rpl::event_stream _sendVoiceRequests; + + int _centerY = 0; + QRect _redCircleRect; + + rpl::variable _recording = false; + bool _inField = false; + int _recordingSamples = 0; + + const style::font &_cancelFont; + int _recordCancelWidth; + + rpl::lifetime _recordingLifetime; + + // This can animate for a very long time (like in music playing), + // so it should be a Basic, not a Simple animation. + Ui::Animations::Basic _recordingAnimation; + anim::value _recordingLevel; + +}; + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 855fd3d946..ed8a05b06c 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -339,6 +339,7 @@ historyRecordSignalMax: 12px; historyRecordCancel: windowSubTextFg; historyRecordCancelActive: windowActiveTextFg; historyRecordFont: font(13px); +historyRecordDurationSkip: 12px; historyRecordDurationFg: historyComposeAreaFg; historyRecordTextTop: 14px;