Created voice record bar as separated history view class.

This commit is contained in:
23rd 2020-10-02 21:14:41 +03:00 committed by John Preston
parent 6d775d6f45
commit 112dea8594
4 changed files with 375 additions and 0 deletions

View file

@ -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

View file

@ -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<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
std::shared_ptr<Ui::SendButton> send,
int recorderHeight)
: RpWidget(parent)
, _controller(controller)
, _wrap(std::make_unique<Ui::RpWidget>(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<QEvent*> 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<QEvent*> e) {
return isTypeRecord()
&& (e->type() == QEvent::MouseMove
|| e->type() == QEvent::MouseButtonRelease);
}) | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseMove) {
const auto mouse = static_cast<QMouseEvent*>(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<SendActionUpdate> VoiceRecordBar::sendActionUpdates() const {
return _sendActionUpdates.events();
}
rpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {
return _sendVoiceRequests.events();
}
bool VoiceRecordBar::isRecording() const {
return _recording.current();
}
void VoiceRecordBar::finishAnimating() {
_recordingAnimation.stop();
}
rpl::producer<bool> VoiceRecordBar::recordingStateChanges() const {
return _recording.changes();
}
rpl::producer<> VoiceRecordBar::startRecordingRequests() const {
return _send->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return isTypeRecord() && (e->type() == QEvent::MouseButtonPress);
}) | rpl::to_empty;
}
bool VoiceRecordBar::isTypeRecord() const {
return (_send->type() == Ui::SendButton::Type::Record);
}
} // namespace HistoryView::Controls

View file

@ -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<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;
[[nodiscard]] rpl::producer<> startRecordingRequests() const;
[[nodiscard]] bool isRecording() const;
VoiceRecordBar(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
std::shared_ptr<Ui::SendButton> 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<Window::SessionController*> _controller;
const std::unique_ptr<Ui::RpWidget> _wrap;
const std::shared_ptr<Ui::SendButton> _send;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
int _centerY = 0;
QRect _redCircleRect;
rpl::variable<bool> _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

View file

@ -339,6 +339,7 @@ historyRecordSignalMax: 12px;
historyRecordCancel: windowSubTextFg;
historyRecordCancelActive: windowActiveTextFg;
historyRecordFont: font(13px);
historyRecordDurationSkip: 12px;
historyRecordDurationFg: historyComposeAreaFg;
historyRecordTextTop: 14px;