"Fossies" - the Fresh Open Source Software Archive 
Member "tdesktop-2.6.0/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp" (23 Feb 2021, 44690 Bytes) of package /linux/misc/tdesktop-2.6.0.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "history_view_voice_record_bar.cpp" see the
Fossies "Dox" file reference documentation and the last
Fossies "Diffs" side-by-side code changes report:
2.5.8_vs_2.5.9.
1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/view/controls/history_view_voice_record_bar.h"
9
10 #include "api/api_send_progress.h"
11 #include "base/event_filter.h"
12 #include "base/openssl_help.h"
13 #include "base/unixtime.h"
14 #include "boxes/confirm_box.h"
15 #include "core/application.h"
16 #include "data/data_document.h"
17 #include "data/data_document_media.h"
18 #include "data/data_session.h"
19 #include "history/history_item_components.h"
20 #include "history/view/controls/history_view_voice_record_button.h"
21 #include "lang/lang_keys.h"
22 #include "main/main_session.h"
23 #include "mainwidget.h" // MainWidget::stopAndClosePlayer
24 #include "mainwindow.h"
25 #include "media/audio/media_audio.h"
26 #include "media/audio/media_audio_capture.h"
27 #include "media/player/media_player_button.h"
28 #include "media/player/media_player_instance.h"
29 #include "styles/style_chat.h"
30 #include "styles/style_layers.h"
31 #include "styles/style_media_player.h"
32 #include "ui/controls/send_button.h"
33 #include "ui/effects/animation_value.h"
34 #include "ui/effects/ripple_animation.h"
35 #include "ui/text/format_values.h"
36 #include "window/window_session_controller.h"
37
38 namespace HistoryView::Controls {
39
40 namespace {
41
42 using SendActionUpdate = VoiceRecordBar::SendActionUpdate;
43 using VoiceToSend = VoiceRecordBar::VoiceToSend;
44
45 constexpr auto kAudioVoiceUpdateView = crl::time(200);
46 constexpr auto kLockDelay = crl::time(100);
47 constexpr auto kRecordingUpdateDelta = crl::time(100);
48 constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes
49 constexpr auto kMaxSamples =
50 ::Media::Player::kDefaultFrequency * kAudioVoiceMaxLength;
51
52 constexpr auto kInactiveWaveformBarAlpha = int(255 * 0.6);
53
54 constexpr auto kPrecision = 10;
55
56 constexpr auto kLockArcAngle = 15.;
57
58 constexpr auto kHideWaveformBgOffset = 50;
59
60 enum class FilterType {
61 Continue,
62 ShowBox,
63 Cancel,
64 };
65
66 [[nodiscard]] auto InactiveColor(const QColor &c) {
67 return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
68 }
69
70 [[nodiscard]] auto Progress(int low, int high) {
71 return std::clamp(float64(low) / high, 0., 1.);
72 }
73
74 [[nodiscard]] auto Duration(int samples) {
75 return samples / ::Media::Player::kDefaultFrequency;
76 }
77
78 [[nodiscard]] auto FormatVoiceDuration(int samples) {
79 const int duration = kPrecision
80 * (float64(samples) / ::Media::Player::kDefaultFrequency);
81 const auto durationString = Ui::FormatDurationText(duration / kPrecision);
82 const auto decimalPart = duration % kPrecision;
83 return QString("%1%2%3")
84 .arg(durationString)
85 .arg(QLocale::system().decimalPoint())
86 .arg(decimalPart);
87 }
88
89 [[nodiscard]] std::unique_ptr<VoiceData> ProcessCaptureResult(
90 const ::Media::Capture::Result &data) {
91 auto voiceData = std::make_unique<VoiceData>();
92 voiceData->duration = Duration(data.samples);
93 voiceData->waveform = data.waveform;
94 voiceData->wavemax = voiceData->waveform.empty()
95 ? uchar(0)
96 : *ranges::max_element(voiceData->waveform);
97 return voiceData;
98 }
99
100 [[nodiscard]] not_null<DocumentData*> DummyDocument(
101 not_null<Data::Session*> owner) {
102 return owner->document(
103 openssl::RandomValue<DocumentId>(),
104 uint64(0),
105 QByteArray(),
106 base::unixtime::now(),
107 QVector<MTPDocumentAttribute>(),
108 QString(),
109 QByteArray(),
110 ImageWithLocation(),
111 ImageWithLocation(),
112 owner->session().mainDcId(),
113 int32(0));
114 }
115
116 void PaintWaveform(
117 Painter &p,
118 not_null<const VoiceData*> voiceData,
119 int availableWidth,
120 const QColor &active,
121 const QColor &inactive,
122 float64 progress) {
123 const auto wf = [&]() -> const VoiceWaveform* {
124 if (voiceData->waveform.isEmpty()) {
125 return nullptr;
126 } else if (voiceData->waveform.at(0) < 0) {
127 return nullptr;
128 }
129 return &voiceData->waveform;
130 }();
131
132 const auto samplesCount = wf
133 ? wf->size()
134 : ::Media::Player::kWaveformSamplesCount;
135 const auto activeWidth = std::round(availableWidth * progress);
136
137 const auto &barWidth = st::historyRecordWaveformBar;
138 const auto barFullWidth = barWidth + st::msgWaveformSkip;
139 const auto totalBarsCountF = (float)availableWidth / barFullWidth;
140 const auto totalBarsCount = int(totalBarsCountF);
141 const auto samplesPerBar = samplesCount / totalBarsCountF;
142 const auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;
143 const auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;
144 const auto &bottom = st::msgWaveformMax;
145
146 p.setPen(Qt::NoPen);
147 int barNum = 0;
148 const auto paintBar = [&](const auto &barValue) {
149 const auto barHeight = st::msgWaveformMin + barValue;
150 const auto barTop = (bottom - barHeight) / 2.;
151 const auto barLeft = barNum * barFullWidth;
152 const auto rect = [&](const auto &l, const auto &w) {
153 return QRectF(l, barTop, w, barHeight);
154 };
155
156 if ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {
157 const auto leftWidth = activeWidth - barLeft;
158 const auto rightWidth = barWidth - leftWidth;
159 p.fillRect(rect(barLeft, leftWidth), active);
160 p.fillRect(rect(activeWidth, rightWidth), inactive);
161 } else {
162 const auto &color = (barLeft >= activeWidth) ? inactive : active;
163 p.fillRect(rect(barLeft, barWidth), color);
164 }
165 barNum++;
166 };
167
168 auto barCounter = 0.;
169 auto nextBarNum = 0;
170
171 auto sum = 0;
172 auto maxValue = 0;
173
174 for (auto i = 0; i < samplesCount; i++) {
175 const auto value = wf ? wf->at(i) : 0;
176 if (i != nextBarNum) {
177 maxValue = std::max(maxValue, value);
178 sum += totalBarsCount;
179 continue;
180 }
181
182 // Compute height.
183 sum += totalBarsCount - samplesCount;
184 const auto isSumSmaller = (sum < (totalBarsCount + 1) / 2);
185 if (isSumSmaller) {
186 maxValue = std::max(maxValue, value);
187 }
188 const auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))
189 / barNormValue;
190 maxValue = isSumSmaller ? 0 : value;
191
192 const auto lastBarNum = nextBarNum;
193 while (lastBarNum == nextBarNum) {
194 barCounter += samplesPerBar;
195 nextBarNum = (int)barCounter;
196 paintBar(barValue);
197 }
198 }
199 }
200
201 } // namespace
202
203 class ListenWrap final {
204 public:
205 ListenWrap(
206 not_null<Ui::RpWidget*> parent,
207 not_null<Window::SessionController*> controller,
208 ::Media::Capture::Result &&data,
209 const style::font &font);
210
211 void requestPaintProgress(float64 progress);
212 rpl::producer<> stopRequests() const;
213 ::Media::Capture::Result *data() const;
214
215 void playPause();
216
217 rpl::lifetime &lifetime();
218
219 private:
220 void init();
221 void initPlayButton();
222 void initPlayProgress();
223
224 bool isInPlayer(const ::Media::Player::TrackState &state) const;
225 bool isInPlayer() const;
226
227 int computeTopMargin(int height) const;
228 QRect computeWaveformRect(const QRect ¢erRect) const;
229
230 not_null<Ui::RpWidget*> _parent;
231
232 const not_null<Window::SessionController*> _controller;
233 const not_null<DocumentData*> _document;
234 const std::unique_ptr<VoiceData> _voiceData;
235 const std::shared_ptr<Data::DocumentMedia> _mediaView;
236 const std::unique_ptr<::Media::Capture::Result> _data;
237 const style::IconButton &_stDelete;
238 const base::unique_qptr<Ui::IconButton> _delete;
239 const style::font &_durationFont;
240 const QString _duration;
241 const int _durationWidth;
242 const style::MediaPlayerButton &_playPauseSt;
243 const base::unique_qptr<Ui::AbstractButton> _playPauseButton;
244 const QColor _activeWaveformBar;
245 const QColor _inactiveWaveformBar;
246
247 bool _isShowAnimation = true;
248
249 QRect _waveformBgRect;
250 QRect _waveformBgFinalCenterRect;
251 QRect _waveformFgRect;
252
253 ::Media::Player::PlayButtonLayout _playPause;
254
255 anim::value _playProgress;
256
257 rpl::variable<float64> _showProgress = 0.;
258
259 rpl::lifetime _lifetime;
260
261 };
262
263 ListenWrap::ListenWrap(
264 not_null<Ui::RpWidget*> parent,
265 not_null<Window::SessionController*> controller,
266 ::Media::Capture::Result &&data,
267 const style::font &font)
268 : _parent(parent)
269 , _controller(controller)
270 , _document(DummyDocument(&_controller->session().data()))
271 , _voiceData(ProcessCaptureResult(data))
272 , _mediaView(_document->createMediaView())
273 , _data(std::make_unique<::Media::Capture::Result>(std::move(data)))
274 , _stDelete(st::historyRecordDelete)
275 , _delete(base::make_unique_q<Ui::IconButton>(parent, _stDelete))
276 , _durationFont(font)
277 , _duration(Ui::FormatDurationText(
278 float64(_data->samples) / ::Media::Player::kDefaultFrequency))
279 , _durationWidth(_durationFont->width(_duration))
280 , _playPauseSt(st::mediaPlayerButton)
281 , _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))
282 , _activeWaveformBar(st::historyRecordVoiceFgActiveIcon->c)
283 , _inactiveWaveformBar(InactiveColor(_activeWaveformBar))
284 , _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) {
285 init();
286 }
287
288 void ListenWrap::init() {
289 auto deleteShow = _showProgress.value(
290 ) | rpl::map([](auto value) {
291 return value == 1.;
292 }) | rpl::distinct_until_changed();
293 _delete->showOn(std::move(deleteShow));
294
295 _parent->sizeValue(
296 ) | rpl::start_with_next([=](QSize size) {
297 _waveformBgRect = QRect({ 0, 0 }, size)
298 .marginsRemoved(st::historyRecordWaveformBgMargins);
299 {
300 const auto m = _stDelete.width + _waveformBgRect.height() / 2;
301 _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved(
302 style::margins(m, 0, m, 0));
303 }
304 {
305 const auto &play = _playPauseSt.playOuter;
306 const auto &final = _waveformBgFinalCenterRect;
307 _playPauseButton->moveToLeft(
308 final.x() - (final.height() - play.width()) / 2,
309 final.y());
310 }
311 _waveformFgRect = computeWaveformRect(_waveformBgFinalCenterRect);
312 }, _lifetime);
313
314 _parent->paintRequest(
315 ) | rpl::start_with_next([=](const QRect &clip) {
316 Painter p(_parent);
317 PainterHighQualityEnabler hq(p);
318 const auto progress = _showProgress.current();
319 p.setOpacity(progress);
320 if (progress > 0. && progress < 1.) {
321 _stDelete.icon.paint(p, _stDelete.iconPosition, _parent->width());
322 }
323
324 {
325 const auto hideOffset = _isShowAnimation
326 ? 0
327 : anim::interpolate(kHideWaveformBgOffset, 0, progress);
328 const auto deleteIconLeft = _stDelete.iconPosition.x();
329 const auto bgRectRight = anim::interpolate(
330 deleteIconLeft,
331 _stDelete.width,
332 _isShowAnimation ? progress : 1.);
333 const auto bgRectLeft = anim::interpolate(
334 _parent->width() - deleteIconLeft - _waveformBgRect.height(),
335 _stDelete.width,
336 _isShowAnimation ? progress : 1.);
337 const auto bgRectMargins = style::margins(
338 bgRectLeft - hideOffset,
339 0,
340 bgRectRight + hideOffset,
341 0);
342 const auto bgRect = _waveformBgRect.marginsRemoved(bgRectMargins);
343
344 const auto horizontalMargin = bgRect.width() - bgRect.height();
345 const auto bgLeftCircleRect = bgRect.marginsRemoved(
346 style::margins(0, 0, horizontalMargin, 0));
347 const auto bgRightCircleRect = bgRect.marginsRemoved(
348 style::margins(horizontalMargin, 0, 0, 0));
349
350 const auto halfHeight = bgRect.height() / 2;
351 const auto bgCenterRect = bgRect.marginsRemoved(
352 style::margins(halfHeight, 0, halfHeight, 0));
353
354 if (!_isShowAnimation) {
355 p.setOpacity(progress);
356 }
357 p.setPen(Qt::NoPen);
358 p.setBrush(st::historyRecordCancelActive);
359 QPainterPath path;
360 path.setFillRule(Qt::WindingFill);
361 path.addEllipse(bgLeftCircleRect);
362 path.addEllipse(bgRightCircleRect);
363 path.addRect(bgCenterRect);
364 p.drawPath(path);
365
366 // Duration paint.
367 {
368 p.setFont(_durationFont);
369 p.setPen(st::historyRecordVoiceFgActiveIcon);
370
371 const auto top = computeTopMargin(_durationFont->ascent);
372 const auto rect = bgCenterRect.marginsRemoved(
373 style::margins(
374 bgCenterRect.width() - _durationWidth,
375 top,
376 0,
377 top));
378 p.drawText(rect, style::al_left, _duration);
379 }
380
381 // Waveform paint.
382 {
383 const auto rect = (progress == 1.)
384 ? _waveformFgRect
385 : computeWaveformRect(bgCenterRect);
386 if (rect.width() > 0) {
387 p.translate(rect.topLeft());
388 PaintWaveform(
389 p,
390 _voiceData.get(),
391 rect.width(),
392 _activeWaveformBar,
393 _inactiveWaveformBar,
394 _playProgress.current());
395 p.resetTransform();
396 }
397 }
398 }
399 }, _lifetime);
400
401 initPlayButton();
402 initPlayProgress();
403 }
404
405 void ListenWrap::initPlayButton() {
406 using namespace ::Media::Player;
407 using State = TrackState;
408
409 _mediaView->setBytes(_data->bytes);
410 _document->type = VoiceDocument;
411
412 const auto &play = _playPauseSt.playOuter;
413 const auto &width = _waveformBgFinalCenterRect.height();
414 _playPauseButton->resize(width, width);
415 _playPauseButton->show();
416
417 _playPauseButton->paintRequest(
418 ) | rpl::start_with_next([=](const QRect &clip) {
419 Painter p(_playPauseButton);
420
421 const auto progress = _showProgress.current();
422 p.translate(width / 2, width / 2);
423 if (progress < 1.) {
424 p.scale(progress, progress);
425 }
426 p.translate(-play.width() / 2, -play.height() / 2);
427 _playPause.paint(p, st::historyRecordVoiceFgActiveIcon);
428 }, _playPauseButton->lifetime());
429
430 _playPauseButton->setClickedCallback([=] {
431 instance()->playPause({ _document, FullMsgId() });
432 });
433
434 const auto showPause = _lifetime.make_state<rpl::variable<bool>>(false);
435 showPause->changes(
436 ) | rpl::start_with_next([=](bool pause) {
437 _playPause.setState(pause
438 ? PlayButtonLayout::State::Pause
439 : PlayButtonLayout::State::Play);
440 }, _lifetime);
441
442 instance()->updatedNotifier(
443 ) | rpl::start_with_next([=](const State &state) {
444 if (isInPlayer(state)) {
445 *showPause = ShowPauseIcon(state.state);
446 } else if (showPause->current()) {
447 *showPause = false;
448 }
449 }, _lifetime);
450
451 instance()->stops(
452 AudioMsgId::Type::Voice
453 ) | rpl::start_with_next([=] {
454 *showPause = false;
455 }, _lifetime);
456
457 const auto weak = Ui::MakeWeak(_controller->content().get());
458 _lifetime.add([=] {
459 if (weak && isInPlayer()) {
460 weak->stopAndClosePlayer();
461 }
462 });
463 }
464
465 void ListenWrap::initPlayProgress() {
466 using namespace ::Media::Player;
467 using State = TrackState;
468
469 const auto animation = _lifetime.make_state<Ui::Animations::Basic>();
470 const auto isPointer = _lifetime.make_state<rpl::variable<bool>>(false);
471 const auto &voice = AudioMsgId::Type::Voice;
472
473 const auto updateCursor = [=](const QPoint &p) {
474 *isPointer = isInPlayer() ? _waveformFgRect.contains(p) : false;
475 };
476
477 rpl::merge(
478 instance()->startsPlay(voice) | rpl::map_to(true),
479 instance()->stops(voice) | rpl::map_to(false)
480 ) | rpl::start_with_next([=](bool play) {
481 _parent->setMouseTracking(isInPlayer() && play);
482 updateCursor(_parent->mapFromGlobal(QCursor::pos()));
483 }, _lifetime);
484
485 instance()->updatedNotifier(
486 ) | rpl::start_with_next([=](const State &state) {
487 if (!isInPlayer(state)) {
488 return;
489 }
490 const auto progress = state.length
491 ? Progress(state.position, state.length)
492 : 0.;
493 if (IsStopped(state.state)) {
494 _playProgress = anim::value();
495 } else {
496 _playProgress.start(progress);
497 }
498 animation->start();
499 }, _lifetime);
500
501 auto animationCallback = [=](crl::time now) {
502 if (anim::Disabled()) {
503 now += kAudioVoiceUpdateView;
504 }
505
506 const auto dt = (now - animation->started())
507 / float64(kAudioVoiceUpdateView);
508 if (dt >= 1.) {
509 animation->stop();
510 _playProgress.finish();
511 } else {
512 _playProgress.update(std::min(dt, 1.), anim::linear);
513 }
514 _parent->update(_waveformFgRect);
515 return (dt < 1.);
516 };
517 animation->init(std::move(animationCallback));
518
519 const auto isPressed = _lifetime.make_state<bool>(false);
520
521 isPointer->changes(
522 ) | rpl::start_with_next([=](bool pointer) {
523 _parent->setCursor(pointer ? style::cur_pointer : style::cur_default);
524 }, _lifetime);
525
526 _parent->events(
527 ) | rpl::filter([=](not_null<QEvent*> e) {
528 return (e->type() == QEvent::MouseMove
529 || e->type() == QEvent::MouseButtonPress
530 || e->type() == QEvent::MouseButtonRelease);
531 }) | rpl::start_with_next([=](not_null<QEvent*> e) {
532 if (!isInPlayer()) {
533 return;
534 }
535
536 const auto type = e->type();
537 const auto isMove = (type == QEvent::MouseMove);
538 const auto &pos = static_cast<QMouseEvent*>(e.get())->pos();
539 if (*isPressed) {
540 *isPointer = true;
541 } else if (isMove) {
542 updateCursor(pos);
543 }
544 if (type == QEvent::MouseButtonPress) {
545 if (isPointer->current() && !(*isPressed)) {
546 instance()->startSeeking(voice);
547 *isPressed = true;
548 }
549 } else if (*isPressed) {
550 const auto &rect = _waveformFgRect;
551 const auto left = float64(pos.x() - rect.x());
552 const auto progress = Progress(left, rect.width());
553 const auto isRelease = (type == QEvent::MouseButtonRelease);
554 if (isRelease || isMove) {
555 _playProgress = anim::value(progress, progress);
556 _parent->update(_waveformFgRect);
557 if (isRelease) {
558 instance()->finishSeeking(voice, progress);
559 *isPressed = false;
560 }
561 }
562 }
563
564 }, _lifetime);
565 }
566
567
568 bool ListenWrap::isInPlayer(const ::Media::Player::TrackState &state) const {
569 return (state.id && (state.id.audio() == _document));
570 }
571
572 bool ListenWrap::isInPlayer() const {
573 using Type = AudioMsgId::Type;
574 return isInPlayer(::Media::Player::instance()->getState(Type::Voice));
575 }
576
577 void ListenWrap::playPause() {
578 _playPauseButton->clicked(Qt::NoModifier, Qt::LeftButton);
579 }
580
581 QRect ListenWrap::computeWaveformRect(const QRect ¢erRect) const {
582 const auto top = computeTopMargin(st::msgWaveformMax);
583 const auto left = (_playPauseSt.playOuter.width() + centerRect.height())
584 / 2;
585 const auto right = st::historyRecordWaveformRightSkip + _durationWidth;
586 return centerRect.marginsRemoved(style::margins(left, top, right, top));
587 }
588
589 int ListenWrap::computeTopMargin(int height) const {
590 return (_waveformBgRect.height() - height) / 2;
591 }
592
593 void ListenWrap::requestPaintProgress(float64 progress) {
594 _isShowAnimation = (_showProgress.current() < progress);
595 _showProgress = progress;
596 }
597
598 rpl::producer<> ListenWrap::stopRequests() const {
599 return _delete->clicks() | rpl::to_empty;
600 }
601
602 ::Media::Capture::Result *ListenWrap::data() const {
603 return _data.get();
604 }
605
606 rpl::lifetime &ListenWrap::lifetime() {
607 return _lifetime;
608 }
609
610 class RecordLock final : public Ui::RippleButton {
611 public:
612 RecordLock(not_null<Ui::RpWidget*> parent);
613
614 void requestPaintProgress(float64 progress);
615 void requestPaintLockToStopProgress(float64 progress);
616
617 [[nodiscard]] rpl::producer<> locks() const;
618 [[nodiscard]] bool isLocked() const;
619 [[nodiscard]] bool isStopState() const;
620
621 [[nodiscard]] float64 lockToStopProgress() const;
622
623 protected:
624 QImage prepareRippleMask() const override;
625 QPoint prepareRippleStartPosition() const override;
626
627 private:
628 void init();
629
630 void drawProgress(Painter &p);
631 void setProgress(float64 progress);
632 void startLockingAnimation(float64 to);
633
634 const QRect _rippleRect;
635 const QPen _arcPen;
636
637 Ui::Animations::Simple _lockEnderAnimation;
638
639 float64 _lockToStopProgress = 0.;
640 rpl::variable<float64> _progress = 0.;
641 };
642
643 RecordLock::RecordLock(not_null<Ui::RpWidget*> parent)
644 : RippleButton(parent, st::defaultRippleAnimation)
645 , _rippleRect(QRect(
646 0,
647 0,
648 st::historyRecordLockTopShadow.width(),
649 st::historyRecordLockTopShadow.width())
650 .marginsRemoved(st::historyRecordLockRippleMargin))
651 , _arcPen(
652 st::historyRecordLockIconFg,
653 st::historyRecordLockIconLineWidth,
654 Qt::SolidLine,
655 Qt::SquareCap,
656 Qt::RoundJoin) {
657 init();
658 }
659
660 void RecordLock::init() {
661 shownValue(
662 ) | rpl::start_with_next([=](bool shown) {
663 resize(
664 st::historyRecordLockTopShadow.width(),
665 st::historyRecordLockSize.height());
666 if (!shown) {
667 setCursor(style::cur_default);
668 setAttribute(Qt::WA_TransparentForMouseEvents, true);
669 _lockEnderAnimation.stop();
670 _lockToStopProgress = 0.;
671 _progress = 0.;
672 }
673 }, lifetime());
674
675 paintRequest(
676 ) | rpl::start_with_next([=](const QRect &clip) {
677 Painter p(this);
678 if (isLocked()) {
679 const auto top = anim::interpolate(
680 0,
681 height() - st::historyRecordLockTopShadow.height() * 2,
682 _lockToStopProgress);
683 p.translate(0, top);
684 drawProgress(p);
685 return;
686 }
687 drawProgress(p);
688 }, lifetime());
689 }
690
691 void RecordLock::drawProgress(Painter &p) {
692 const auto progress = _progress.current();
693
694 const auto &originTop = st::historyRecordLockTop;
695 const auto &originBottom = st::historyRecordLockBottom;
696 const auto &originBody = st::historyRecordLockBody;
697 const auto &shadowTop = st::historyRecordLockTopShadow;
698 const auto &shadowBottom = st::historyRecordLockBottomShadow;
699 const auto &shadowBody = st::historyRecordLockBodyShadow;
700 const auto &shadowMargins = st::historyRecordLockMargin;
701
702 const auto bottomMargin = anim::interpolate(
703 0,
704 rect().height() - shadowTop.height() - shadowBottom.height(),
705 progress);
706
707 const auto topMargin = anim::interpolate(
708 rect().height() / 4,
709 0,
710 progress);
711
712 const auto full = rect().marginsRemoved(
713 style::margins(0, topMargin, 0, bottomMargin));
714 const auto inner = full.marginsRemoved(shadowMargins);
715 const auto content = inner.marginsRemoved(style::margins(
716 0,
717 originTop.height(),
718 0,
719 originBottom.height()));
720 const auto contentShadow = full.marginsRemoved(style::margins(
721 0,
722 shadowTop.height(),
723 0,
724 shadowBottom.height()));
725
726 const auto w = full.width();
727 {
728 shadowTop.paint(p, full.topLeft(), w);
729 originTop.paint(p, inner.topLeft(), w);
730 }
731 {
732 const auto shadowPos = QPoint(
733 full.x(),
734 contentShadow.y() + contentShadow.height());
735 const auto originPos = QPoint(
736 inner.x(),
737 content.y() + content.height());
738 shadowBottom.paint(p, shadowPos, w);
739 originBottom.paint(p, originPos, w);
740 }
741 {
742 shadowBody.fill(p, contentShadow);
743 originBody.fill(p, content);
744 }
745 {
746 const auto &arrow = st::historyRecordLockArrow;
747 const auto arrowRect = QRect(
748 inner.x(),
749 content.y() + content.height() - arrow.height() / 2,
750 inner.width(),
751 arrow.height());
752 p.setOpacity(1. - progress);
753 arrow.paintInCenter(p, arrowRect);
754 p.setOpacity(1.);
755 }
756 if (isLocked()) {
757 paintRipple(p, _rippleRect.x(), _rippleRect.y());
758 }
759 {
760 PainterHighQualityEnabler hq(p);
761 const auto &arcOffset = st::historyRecordLockIconLineSkip;
762 const auto &size = st::historyRecordLockIconSize;
763
764 const auto arcWidth = size.width() - arcOffset * 2;
765 const auto &arcHeight = st::historyRecordLockIconArcHeight;
766
767 const auto &blockHeight = st::historyRecordLockIconBottomHeight;
768
769 const auto blockRectWidth = anim::interpolateF(
770 size.width(),
771 st::historyRecordStopIconWidth,
772 _lockToStopProgress);
773 const auto blockRectHeight = anim::interpolateF(
774 blockHeight,
775 st::historyRecordStopIconWidth,
776 _lockToStopProgress);
777 const auto blockRectTop = anim::interpolateF(
778 size.height() - blockHeight,
779 std::round((size.height() - blockRectHeight) / 2.),
780 _lockToStopProgress);
781
782 const auto blockRect = QRectF(
783 (size.width() - blockRectWidth) / 2,
784 blockRectTop,
785 blockRectWidth,
786 blockRectHeight);
787 const auto &lineHeight = st::historyRecordLockIconLineHeight;
788
789 p.setPen(Qt::NoPen);
790 p.setBrush(st::historyRecordLockIconFg);
791 p.translate(
792 inner.x() + (inner.width() - size.width()) / 2,
793 inner.y() + (originTop.height() * 2 - size.height()) / 2);
794 {
795 const auto xRadius = anim::interpolate(2, 3, _lockToStopProgress);
796 p.drawRoundedRect(blockRect, xRadius, 3);
797 }
798
799 const auto offsetTranslate = _lockToStopProgress *
800 (lineHeight + arcHeight + _arcPen.width() * 2);
801 p.translate(
802 size.width() - arcOffset,
803 blockRect.y() + offsetTranslate);
804
805 if (progress < 1. && progress > 0.) {
806 p.rotate(kLockArcAngle * progress);
807 }
808
809 p.setPen(_arcPen);
810 const auto rLine = QLineF(0, 0, 0, -lineHeight);
811 p.drawLine(rLine);
812
813 p.drawArc(
814 -arcWidth,
815 rLine.dy() - arcHeight - _arcPen.width() + rLine.y1(),
816 arcWidth,
817 arcHeight * 2,
818 0,
819 180 * 16);
820
821 const auto lockProgress = 1. - _lockToStopProgress;
822 if (progress == 1. && lockProgress < 1.) {
823 p.drawLine(
824 -arcWidth,
825 rLine.y2(),
826 -arcWidth,
827 rLine.dy() * lockProgress);
828 }
829 }
830 }
831
832 void RecordLock::startLockingAnimation(float64 to) {
833 auto callback = [=](float64 value) { setProgress(value); };
834 const auto &duration = st::historyRecordVoiceShowDuration;
835 _lockEnderAnimation.start(std::move(callback), 0., to, duration);
836 }
837
838 void RecordLock::requestPaintProgress(float64 progress) {
839 if (isHidden()
840 || isLocked()
841 || _lockEnderAnimation.animating()
842 || (_progress.current() == progress)) {
843 return;
844 }
845 if (!_progress.current() && (progress > .3)) {
846 startLockingAnimation(progress);
847 return;
848 }
849 setProgress(progress);
850 }
851
852 void RecordLock::requestPaintLockToStopProgress(float64 progress) {
853 _lockToStopProgress = progress;
854 if (isStopState()) {
855 setCursor(style::cur_pointer);
856 setAttribute(Qt::WA_TransparentForMouseEvents, false);
857
858 resize(
859 st::historyRecordLockTopShadow.width(),
860 st::historyRecordLockTopShadow.width());
861 }
862 update();
863 }
864
865 float64 RecordLock::lockToStopProgress() const {
866 return _lockToStopProgress;
867 }
868
869 void RecordLock::setProgress(float64 progress) {
870 _progress = progress;
871 update();
872 }
873
874 bool RecordLock::isLocked() const {
875 return _progress.current() == 1.;
876 }
877
878 bool RecordLock::isStopState() const {
879 return isLocked() && (_lockToStopProgress == 1.);
880 }
881
882 rpl::producer<> RecordLock::locks() const {
883 return _progress.changes(
884 ) | rpl::filter([=] { return isLocked(); }) | rpl::to_empty;
885 }
886
887 QImage RecordLock::prepareRippleMask() const {
888 return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
889 }
890
891 QPoint RecordLock::prepareRippleStartPosition() const {
892 return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
893 }
894
895 class CancelButton final : public Ui::RippleButton {
896 public:
897 CancelButton(not_null<Ui::RpWidget*> parent, int height);
898
899 void requestPaintProgress(float64 progress);
900
901 protected:
902 QImage prepareRippleMask() const override;
903 QPoint prepareRippleStartPosition() const override;
904
905 private:
906 void init();
907
908 const int _width;
909 const QRect _rippleRect;
910
911 rpl::variable<float64> _showProgress = 0.;
912
913 Ui::Text::String _text;
914
915 };
916
917 CancelButton::CancelButton(not_null<Ui::RpWidget*> parent, int height)
918 : Ui::RippleButton(parent, st::defaultLightButton.ripple)
919 , _width(st::historyRecordCancelButtonWidth)
920 , _rippleRect(QRect(0, (height - _width) / 2, _width, _width))
921 , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now).toUpper()) {
922 resize(_width, height);
923 init();
924 }
925
926 void CancelButton::init() {
927 _showProgress.value(
928 ) | rpl::map(rpl::mappers::_1 > 0.) | rpl::distinct_until_changed(
929 ) | rpl::start_with_next([=](bool hasProgress) {
930 setVisible(hasProgress);
931 }, lifetime());
932
933 paintRequest(
934 ) | rpl::start_with_next([=] {
935 Painter p(this);
936
937 p.setOpacity(_showProgress.current());
938
939 paintRipple(p, _rippleRect.x(), _rippleRect.y());
940
941 p.setPen(st::historyRecordCancelButtonFg);
942 _text.draw(
943 p,
944 0,
945 (height() - _text.minHeight()) / 2,
946 width(),
947 style::al_center);
948 }, lifetime());
949 }
950
951 QImage CancelButton::prepareRippleMask() const {
952 return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
953 }
954
955 QPoint CancelButton::prepareRippleStartPosition() const {
956 return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
957 }
958
959 void CancelButton::requestPaintProgress(float64 progress) {
960 _showProgress = progress;
961 update();
962 }
963
964 VoiceRecordBar::VoiceRecordBar(
965 not_null<Ui::RpWidget*> parent,
966 not_null<Ui::RpWidget*> sectionWidget,
967 not_null<Window::SessionController*> controller,
968 std::shared_ptr<Ui::SendButton> send,
969 int recorderHeight)
970 : RpWidget(parent)
971 , _sectionWidget(sectionWidget)
972 , _controller(controller)
973 , _send(send)
974 , _lock(std::make_unique<RecordLock>(sectionWidget))
975 , _level(std::make_unique<VoiceRecordButton>(
976 sectionWidget,
977 _controller->widget()->leaveEvents()))
978 , _cancel(std::make_unique<CancelButton>(this, recorderHeight))
979 , _startTimer([=] { startRecording(); })
980 , _message(
981 st::historyRecordTextStyle,
982 tr::lng_record_cancel(tr::now),
983 TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })
984 , _cancelFont(st::historyRecordFont) {
985 resize(QSize(parent->width(), recorderHeight));
986 init();
987 hideFast();
988 }
989
990 VoiceRecordBar::VoiceRecordBar(
991 not_null<Ui::RpWidget*> parent,
992 not_null<Window::SessionController*> controller,
993 std::shared_ptr<Ui::SendButton> send,
994 int recorderHeight)
995 : VoiceRecordBar(parent, parent, controller, send, recorderHeight) {
996 }
997
998 VoiceRecordBar::~VoiceRecordBar() {
999 if (isRecording()) {
1000 stopRecording(StopType::Cancel);
1001 }
1002 }
1003
1004 void VoiceRecordBar::updateMessageGeometry() {
1005 const auto left = _durationRect.x()
1006 + _durationRect.width()
1007 + st::historyRecordTextLeft;
1008 const auto right = width()
1009 - _send->width()
1010 - st::historyRecordTextRight;
1011 const auto textWidth = _message.maxWidth();
1012 const auto width = ((right - left) < textWidth)
1013 ? st::historyRecordTextWidthForWrap
1014 : textWidth;
1015 const auto countLines = std::ceil((float)textWidth / width);
1016 const auto textHeight = _message.minHeight() * countLines;
1017 _messageRect = QRect(
1018 left + (right - left - width) / 2,
1019 (height() - textHeight) / 2,
1020 width,
1021 textHeight);
1022 }
1023
1024 void VoiceRecordBar::updateLockGeometry() {
1025 const auto right = anim::interpolate(
1026 -_lock->width(),
1027 st::historyRecordLockPosition.x(),
1028 _showLockAnimation.value(_lockShowing.current() ? 1. : 0.));
1029 _lock->moveToRight(right, _lock->y());
1030 }
1031
1032 void VoiceRecordBar::init() {
1033 // Keep VoiceRecordBar behind SendButton.
1034 rpl::single(
1035 ) | rpl::then(
1036 _send->events(
1037 ) | rpl::filter([](not_null<QEvent*> e) {
1038 return e->type() == QEvent::ZOrderChange;
1039 }) | rpl::to_empty
1040 ) | rpl::start_with_next([=] {
1041 orderControls();
1042 }, lifetime());
1043
1044 shownValue(
1045 ) | rpl::start_with_next([=](bool show) {
1046 if (!show) {
1047 finish();
1048 }
1049 }, lifetime());
1050
1051 sizeValue(
1052 ) | rpl::start_with_next([=](QSize size) {
1053 _centerY = size.height() / 2;
1054 {
1055 const auto maxD = st::historyRecordSignalRadius * 2;
1056 const auto point = _centerY - st::historyRecordSignalRadius;
1057 _redCircleRect = { point, point, maxD, maxD };
1058 }
1059 {
1060 const auto durationLeft = _redCircleRect.x()
1061 + _redCircleRect.width()
1062 + st::historyRecordDurationSkip;
1063 const auto &ascent = _cancelFont->ascent;
1064 _durationRect = QRect(
1065 durationLeft,
1066 _redCircleRect.y() - (ascent - _redCircleRect.height()) / 2,
1067 _cancelFont->width(FormatVoiceDuration(kMaxSamples)),
1068 ascent);
1069 }
1070 _cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0);
1071 updateMessageGeometry();
1072 updateLockGeometry();
1073 }, lifetime());
1074
1075 paintRequest(
1076 ) | rpl::start_with_next([=](const QRect &clip) {
1077 Painter p(this);
1078 if (_showAnimation.animating()) {
1079 p.setOpacity(showAnimationRatio());
1080 }
1081 p.fillRect(clip, st::historyComposeAreaBg);
1082
1083 p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio()));
1084 const auto opacity = p.opacity();
1085 _cancel->requestPaintProgress(_lock->isStopState()
1086 ? (opacity * _lock->lockToStopProgress())
1087 : 0.);
1088
1089 if (!opacity) {
1090 return;
1091 }
1092 if (clip.intersects(_messageRect)) {
1093 // The message should be painted first to avoid flickering.
1094 drawMessage(p, activeAnimationRatio());
1095 }
1096 if (clip.intersects(_durationRect)) {
1097 drawDuration(p);
1098 }
1099 if (clip.intersects(_redCircleRect)) {
1100 // Should be the last to be drawn.
1101 drawRedCircle(p);
1102 }
1103 }, lifetime());
1104
1105 _inField.changes(
1106 ) | rpl::start_with_next([=](bool value) {
1107 activeAnimate(value);
1108 }, lifetime());
1109
1110 _lockShowing.changes(
1111 ) | rpl::start_with_next([=](bool show) {
1112 const auto to = show ? 1. : 0.;
1113 const auto from = show ? 0. : 1.;
1114 const auto &duration = st::historyRecordLockShowDuration;
1115 _lock->show();
1116 auto callback = [=](float64 value) {
1117 updateLockGeometry();
1118 if (value == 0. && !show) {
1119 _lock->hide();
1120 } else if (value == 1. && show) {
1121 computeAndSetLockProgress(QCursor::pos());
1122 }
1123 };
1124 _showLockAnimation.start(std::move(callback), from, to, duration);
1125 }, lifetime());
1126
1127 _lock->setClickedCallback([=] {
1128 if (!_lock->isStopState()) {
1129 return;
1130 }
1131
1132 ::Media::Capture::instance()->startedChanges(
1133 ) | rpl::filter([=](bool capturing) {
1134 return !capturing && _listen;
1135 }) | rpl::take(1) | rpl::start_with_next([=] {
1136 _lockShowing = false;
1137
1138 const auto to = 1.;
1139 const auto &duration = st::historyRecordVoiceShowDuration;
1140 auto callback = [=](float64 value) {
1141 _listen->requestPaintProgress(value);
1142 const auto reverseValue = to - value;
1143 _level->requestPaintProgress(reverseValue);
1144 update();
1145 if (to == value) {
1146 _recordingLifetime.destroy();
1147 }
1148 };
1149 _showListenAnimation.start(std::move(callback), 0., to, duration);
1150 }, lifetime());
1151
1152 stopRecording(StopType::Listen);
1153 });
1154
1155 _lock->locks(
1156 ) | rpl::start_with_next([=] {
1157 _level->setType(VoiceRecordButton::Type::Send);
1158
1159 _level->clicks(
1160 ) | rpl::start_with_next([=] {
1161 stop(true);
1162 }, _recordingLifetime);
1163
1164 rpl::single(
1165 false
1166 ) | rpl::then(
1167 _level->actives()
1168 ) | rpl::start_with_next([=](bool enter) {
1169 _inField = enter;
1170 }, _recordingLifetime);
1171
1172 const auto &duration = st::historyRecordVoiceShowDuration;
1173 const auto from = 0.;
1174 const auto to = 1.;
1175 auto callback = [=](float64 value) {
1176 _lock->requestPaintLockToStopProgress(value);
1177 update();
1178 };
1179 _lockToStopAnimation.start(std::move(callback), from, to, duration);
1180 }, lifetime());
1181
1182 _send->events(
1183 ) | rpl::filter([=](not_null<QEvent*> e) {
1184 return isTypeRecord()
1185 && !isRecording()
1186 && !_showAnimation.animating()
1187 && !_lock->isLocked()
1188 && (e->type() == QEvent::MouseButtonPress
1189 || e->type() == QEvent::MouseButtonRelease);
1190 }) | rpl::start_with_next([=](not_null<QEvent*> e) {
1191 if (e->type() == QEvent::MouseButtonPress) {
1192 if (_startRecordingFilter && _startRecordingFilter()) {
1193 return;
1194 }
1195 _startTimer.callOnce(st::historyRecordVoiceShowDuration);
1196 } else if (e->type() == QEvent::MouseButtonRelease) {
1197 _startTimer.cancel();
1198 }
1199 }, lifetime());
1200
1201 _listenChanges.events(
1202 ) | rpl::filter([=] {
1203 return _listen != nullptr;
1204 }) | rpl::start_with_next([=] {
1205 _listen->stopRequests(
1206 ) | rpl::take(1) | rpl::start_with_next([=] {
1207 hideAnimated();
1208 }, _listen->lifetime());
1209
1210 _listen->lifetime().add([=] { _listenChanges.fire({}); });
1211
1212 installListenStateFilter();
1213 }, lifetime());
1214
1215 _cancel->setClickedCallback([=] {
1216 hideAnimated();
1217 });
1218 }
1219
1220 void VoiceRecordBar::activeAnimate(bool active) {
1221 const auto to = active ? 1. : 0.;
1222 const auto &duration = st::historyRecordVoiceDuration;
1223 if (_activeAnimation.animating()) {
1224 _activeAnimation.change(to, duration);
1225 } else {
1226 auto callback = [=] {
1227 update(_messageRect);
1228 _level->requestPaintColor(activeAnimationRatio());
1229 };
1230 const auto from = active ? 0. : 1.;
1231 _activeAnimation.start(std::move(callback), from, to, duration);
1232 }
1233 }
1234
1235 void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
1236 const auto to = show ? 1. : 0.;
1237 const auto from = show ? 0. : 1.;
1238 const auto &duration = st::historyRecordVoiceShowDuration;
1239 auto animationCallback = [=, callback = std::move(callback)](auto value) {
1240 if (!_listen) {
1241 _level->requestPaintProgress(value);
1242 } else {
1243 _listen->requestPaintProgress(value);
1244 }
1245 update();
1246 if ((show && value == 1.) || (!show && value == 0.)) {
1247 if (callback) {
1248 callback();
1249 }
1250 }
1251 };
1252 _showAnimation.start(std::move(animationCallback), from, to, duration);
1253 }
1254
1255 void VoiceRecordBar::setStartRecordingFilter(Fn<bool()> &&callback) {
1256 _startRecordingFilter = std::move(callback);
1257 }
1258
1259 void VoiceRecordBar::setLockBottom(rpl::producer<int> &&bottom) {
1260 rpl::combine(
1261 std::move(bottom),
1262 _lock->sizeValue() | rpl::map_to(true) // Dummy value.
1263 ) | rpl::start_with_next([=](int value, bool dummy) {
1264 _lock->moveToLeft(_lock->x(), value - _lock->height());
1265 }, lifetime());
1266 }
1267
1268 void VoiceRecordBar::setSendButtonGeometryValue(
1269 rpl::producer<QRect> &&geometry) {
1270 std::move(
1271 geometry
1272 ) | rpl::start_with_next([=](QRect r) {
1273 const auto center = (r.width() - _level->width()) / 2;
1274 _level->moveToLeft(r.x() + center, r.y() + center);
1275 }, lifetime());
1276 }
1277
1278 void VoiceRecordBar::startRecording() {
1279 if (isRecording()) {
1280 return;
1281 }
1282 auto appearanceCallback = [=] {
1283 if(_showAnimation.animating()) {
1284 return;
1285 }
1286
1287 using namespace ::Media::Capture;
1288 if (!instance()->available()) {
1289 stop(false);
1290 return;
1291 }
1292
1293 _lockShowing = true;
1294 startRedCircleAnimation();
1295
1296 _recording = true;
1297 _controller->widget()->setInnerFocus();
1298 instance()->start();
1299 instance()->updated(
1300 ) | rpl::start_with_next_error([=](const Update &update) {
1301 recordUpdated(update.level, update.samples);
1302 }, [=] {
1303 stop(false);
1304 }, _recordingLifetime);
1305 _recordingLifetime.add([=] {
1306 _recording = false;
1307 });
1308 };
1309 visibilityAnimate(true, std::move(appearanceCallback));
1310 show();
1311
1312 _inField = true;
1313
1314 _send->events(
1315 ) | rpl::filter([=](not_null<QEvent*> e) {
1316 return isTypeRecord()
1317 && !_lock->isLocked()
1318 && (e->type() == QEvent::MouseMove
1319 || e->type() == QEvent::MouseButtonRelease);
1320 }) | rpl::start_with_next([=](not_null<QEvent*> e) {
1321 const auto type = e->type();
1322 if (type == QEvent::MouseMove) {
1323 const auto mouse = static_cast<QMouseEvent*>(e.get());
1324 const auto globalPos = mouse->globalPos();
1325 const auto localPos = mapFromGlobal(globalPos);
1326 const auto inField = rect().contains(localPos);
1327 _inField = inField
1328 ? inField
1329 : _level->inCircle(_level->mapFromGlobal(globalPos));
1330
1331 if (_showLockAnimation.animating() || !hasDuration()) {
1332 return;
1333 }
1334 computeAndSetLockProgress(mouse->globalPos());
1335 } else if (type == QEvent::MouseButtonRelease) {
1336 stop(_inField.current());
1337 }
1338 }, _recordingLifetime);
1339 }
1340
1341 void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
1342 _level->requestPaintLevel(level);
1343 _recordingSamples = samples;
1344 if (samples < 0 || samples >= kMaxSamples) {
1345 stop(samples > 0 && _inField.current());
1346 }
1347 Core::App().updateNonIdle();
1348 update(_durationRect);
1349 _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice });
1350 }
1351
1352 void VoiceRecordBar::stop(bool send) {
1353 if (isHidden() && !send) {
1354 return;
1355 }
1356 auto disappearanceCallback = [=] {
1357 hide();
1358
1359 stopRecording(send ? StopType::Send : StopType::Cancel);
1360 };
1361 _lockShowing = false;
1362 visibilityAnimate(false, std::move(disappearanceCallback));
1363 }
1364
1365 void VoiceRecordBar::finish() {
1366 _recordingLifetime.destroy();
1367 _lockShowing = false;
1368 _inField = false;
1369 _redCircleProgress = 0.;
1370 _recordingSamples = 0;
1371
1372 _showAnimation.stop();
1373 _lockToStopAnimation.stop();
1374
1375 _listen = nullptr;
1376
1377 _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
1378 _controller->widget()->setInnerFocus();
1379 }
1380
1381 void VoiceRecordBar::hideFast() {
1382 hide();
1383 _lock->hide();
1384 _level->hide();
1385 stopRecording(StopType::Cancel);
1386 }
1387
1388 void VoiceRecordBar::stopRecording(StopType type) {
1389 using namespace ::Media::Capture;
1390 if (type == StopType::Cancel) {
1391 instance()->stop(crl::guard(this, [=](Result &&data) {
1392 _cancelRequests.fire({});
1393 }));
1394 return;
1395 }
1396 instance()->stop(crl::guard(this, [=](Result &&data) {
1397 if (data.bytes.isEmpty()) {
1398 // Close everything.
1399 stop(false);
1400 return;
1401 }
1402
1403 Window::ActivateWindow(_controller);
1404 const auto duration = Duration(data.samples);
1405 if (type == StopType::Send) {
1406 _sendVoiceRequests.fire({ data.bytes, data.waveform, duration });
1407 } else if (type == StopType::Listen) {
1408 _listen = std::make_unique<ListenWrap>(
1409 this,
1410 _controller,
1411 std::move(data),
1412 _cancelFont);
1413 _listenChanges.fire({});
1414
1415 _lockShowing = false;
1416 }
1417 }));
1418 }
1419
1420 void VoiceRecordBar::drawDuration(Painter &p) {
1421 const auto duration = FormatVoiceDuration(_recordingSamples);
1422 p.setFont(_cancelFont);
1423 p.setPen(st::historyRecordDurationFg);
1424
1425 p.drawText(_durationRect, style::al_left, duration);
1426 }
1427
1428 void VoiceRecordBar::startRedCircleAnimation() {
1429 if (anim::Disabled()) {
1430 return;
1431 }
1432 const auto animation = _recordingLifetime
1433 .make_state<Ui::Animations::Basic>();
1434 animation->init([=](crl::time now) {
1435 const auto diffTime = now - animation->started();
1436 _redCircleProgress = std::abs(std::sin(diffTime / 400.));
1437 update(_redCircleRect);
1438 return true;
1439 });
1440 animation->start();
1441 }
1442
1443 void VoiceRecordBar::drawRedCircle(Painter &p) {
1444 PainterHighQualityEnabler hq(p);
1445 p.setPen(Qt::NoPen);
1446 p.setBrush(st::historyRecordVoiceFgInactive);
1447
1448 const auto opacity = p.opacity();
1449 p.setOpacity(opacity * (1. - _redCircleProgress));
1450 const int radii = st::historyRecordSignalRadius * showAnimationRatio();
1451 const auto center = _redCircleRect.center() + QPoint(1, 1);
1452 p.drawEllipse(center, radii, radii);
1453 p.setOpacity(opacity);
1454 }
1455
1456 void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) {
1457 p.setPen(
1458 anim::pen(
1459 st::historyRecordCancel,
1460 st::historyRecordCancelActive,
1461 1. - recordActive));
1462
1463 const auto opacity = p.opacity();
1464 p.setOpacity(opacity * (1. - _lock->lockToStopProgress()));
1465
1466 _message.draw(
1467 p,
1468 _messageRect.x(),
1469 _messageRect.y(),
1470 _messageRect.width(),
1471 style::al_center);
1472
1473 p.setOpacity(opacity);
1474 }
1475
1476 void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
1477 if (isListenState()) {
1478 const auto data = _listen->data();
1479 _sendVoiceRequests.fire({
1480 data->bytes,
1481 data->waveform,
1482 Duration(data->samples),
1483 options });
1484 }
1485 }
1486
1487 rpl::producer<SendActionUpdate> VoiceRecordBar::sendActionUpdates() const {
1488 return _sendActionUpdates.events();
1489 }
1490
1491 rpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {
1492 return _sendVoiceRequests.events();
1493 }
1494
1495 rpl::producer<> VoiceRecordBar::cancelRequests() const {
1496 return _cancelRequests.events();
1497 }
1498
1499 bool VoiceRecordBar::isRecording() const {
1500 return _recording.current();
1501 }
1502
1503 bool VoiceRecordBar::isActive() const {
1504 return isRecording() || isListenState();
1505 }
1506
1507 void VoiceRecordBar::hideAnimated() {
1508 if (isHidden()) {
1509 return;
1510 }
1511 _lockShowing = false;
1512 visibilityAnimate(false, [=] { hideFast(); });
1513 }
1514
1515 void VoiceRecordBar::finishAnimating() {
1516 _showAnimation.stop();
1517 }
1518
1519 rpl::producer<bool> VoiceRecordBar::recordingStateChanges() const {
1520 return _recording.changes();
1521 }
1522
1523 rpl::producer<bool> VoiceRecordBar::lockShowStarts() const {
1524 return _lockShowing.changes();
1525 }
1526
1527 rpl::producer<not_null<QEvent*>> VoiceRecordBar::lockViewportEvents() const {
1528 return _lock->events(
1529 ) | rpl::filter([=](not_null<QEvent*> e) {
1530 return e->type() == QEvent::Wheel;
1531 });
1532 }
1533
1534 rpl::producer<> VoiceRecordBar::updateSendButtonTypeRequests() const {
1535 return _listenChanges.events();
1536 }
1537
1538 bool VoiceRecordBar::isLockPresent() const {
1539 return _lockShowing.current();
1540 }
1541
1542 bool VoiceRecordBar::isListenState() const {
1543 return _listen != nullptr;
1544 }
1545
1546 bool VoiceRecordBar::isTypeRecord() const {
1547 return (_send->type() == Ui::SendButton::Type::Record);
1548 }
1549
1550 bool VoiceRecordBar::hasDuration() const {
1551 return _recordingSamples > 0;
1552 }
1553
1554 float64 VoiceRecordBar::activeAnimationRatio() const {
1555 return _activeAnimation.value(_inField.current() ? 1. : 0.);
1556 }
1557
1558 void VoiceRecordBar::clearListenState() {
1559 if (isListenState()) {
1560 hideAnimated();
1561 }
1562 }
1563
1564 float64 VoiceRecordBar::showAnimationRatio() const {
1565 // There is no reason to set the final value to zero,
1566 // because at zero this widget is hidden.
1567 return _showAnimation.value(1.);
1568 }
1569
1570 float64 VoiceRecordBar::showListenAnimationRatio() const {
1571 return _showListenAnimation.value(_listen ? 1. : 0.);
1572 }
1573
1574 void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) {
1575 const auto localPos = mapFromGlobal(globalPos);
1576 const auto lower = _lock->height();
1577 const auto higher = 0;
1578 _lock->requestPaintProgress(Progress(localPos.y(), higher - lower));
1579 }
1580
1581 void VoiceRecordBar::orderControls() {
1582 stackUnder(_send.get());
1583 _level->raise();
1584 _lock->raise();
1585 }
1586
1587 void VoiceRecordBar::installListenStateFilter() {
1588 auto keyFilterCallback = [=](not_null<QEvent*> e) {
1589 using Result = base::EventFilterResult;
1590 if (!(_send->type() == Ui::SendButton::Type::Send
1591 || _send->type() == Ui::SendButton::Type::Schedule)) {
1592 return Result::Continue;
1593 }
1594 switch(e->type()) {
1595 case QEvent::KeyPress: {
1596 const auto keyEvent = static_cast<QKeyEvent*>(e.get());
1597 const auto key = keyEvent->key();
1598 const auto isSpace = (key == Qt::Key_Space);
1599 const auto isEnter = (key == Qt::Key_Enter
1600 || key == Qt::Key_Return);
1601 if (isSpace && !keyEvent->isAutoRepeat() && _listen) {
1602 _listen->playPause();
1603 return Result::Cancel;
1604 }
1605 if (isEnter && !_warningShown) {
1606 requestToSendWithOptions({});
1607 return Result::Cancel;
1608 }
1609 return Result::Continue;
1610 }
1611 default: return Result::Continue;
1612 }
1613 };
1614
1615 auto keyFilter = base::install_event_filter(
1616 QCoreApplication::instance(),
1617 std::move(keyFilterCallback));
1618
1619 _listen->lifetime().make_state<base::unique_qptr<QObject>>(
1620 std::move(keyFilter));
1621 }
1622
1623 void VoiceRecordBar::showDiscardBox(
1624 Fn<void()> &&callback,
1625 anim::type animated) {
1626 if (!isActive()) {
1627 return;
1628 }
1629 auto sure = [=, callback = std::move(callback)](Fn<void()> &&close) {
1630 if (animated == anim::type::instant) {
1631 hideFast();
1632 } else {
1633 hideAnimated();
1634 }
1635 close();
1636 _warningShown = false;
1637 if (callback) {
1638 callback();
1639 }
1640 };
1641 Ui::show(Box<ConfirmBox>(
1642 (isListenState()
1643 ? tr::lng_record_listen_cancel_sure
1644 : tr::lng_record_lock_cancel_sure)(tr::now),
1645 tr::lng_record_lock_discard(tr::now),
1646 st::attentionBoxButton,
1647 std::move(sure)));
1648 _warningShown = true;
1649 }
1650
1651 } // namespace HistoryView::Controls