From 8833d3e45b9eafe822284d2f11e18b7eff140265 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 24 Nov 2020 15:54:20 +0300 Subject: [PATCH] Add empty group call panel. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/calls/calls.style | 3 + .../SourceFiles/calls/calls_group_panel.cpp | 462 ++++++++++++++++++ .../SourceFiles/calls/calls_group_panel.h | 96 ++++ Telegram/SourceFiles/calls/calls_instance.cpp | 22 +- Telegram/SourceFiles/calls/calls_instance.h | 5 + 6 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 Telegram/SourceFiles/calls/calls_group_panel.cpp create mode 100644 Telegram/SourceFiles/calls/calls_group_panel.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c54dd9165..a483dabc4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -266,6 +266,8 @@ PRIVATE calls/calls_call.h calls/calls_group_call.cpp calls/calls_group_call.h + calls/calls_group_panel.cpp + calls/calls_group_panel.h calls/calls_emoji_fingerprint.cpp calls/calls_emoji_fingerprint.h calls/calls_instance.cpp diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index ae044b7c9..91441586b 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -399,3 +399,6 @@ callTitleShadow: icon {{ "calls_shadow_controls", windowShadowFg }}; callErrorToast: Toast(defaultToast) { minWidth: 240px; } + +groupCallWidth: 380px; +groupCallHeight: 580px; diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp new file mode 100644 index 000000000..4309519e4 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -0,0 +1,462 @@ +/* +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 "calls/calls_group_panel.h" + +#include "data/data_photo.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/data_file_origin.h" +#include "data/data_photo_media.h" +#include "data/data_cloud_file.h" +#include "data/data_changes.h" +#include "calls/calls_emoji_fingerprint.h" +#include "calls/calls_signal_bars.h" +#include "calls/calls_userpic.h" +#include "calls/calls_video_bubble.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/widgets/window.h" +#include "ui/effects/ripple_animation.h" +#include "ui/image/image.h" +#include "ui/text/format_values.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/platform/ui_platform_utility.h" +#include "ui/toast/toast.h" +#include "ui/empty_userpic.h" +#include "ui/emoji_config.h" +#include "core/application.h" +#include "mainwindow.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "apiwrap.h" +#include "platform/platform_specific.h" +#include "base/platform/base_platform_info.h" +#include "window/main_window.h" +#include "app.h" +#include "webrtc/webrtc_video_track.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" + +#ifdef Q_OS_WIN +#include "ui/platform/win/ui_window_title_win.h" +#endif // Q_OS_WIN + +#include +#include +#include + +namespace Calls { + +class GroupPanel::Button final : public Ui::RippleButton { +public: + Button( + QWidget *parent, + const style::CallButton &stFrom, + const style::CallButton *stTo = nullptr); + + void setProgress(float64 progress); + void setText(rpl::producer text); + +protected: + void paintEvent(QPaintEvent *e) override; + + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + QPoint iconPosition(not_null st) const; + void mixIconMasks(); + + not_null _stFrom; + const style::CallButton *_stTo = nullptr; + float64 _progress = 0.; + + object_ptr _label = { nullptr }; + + QImage _bgMask, _bg; + QPixmap _bgFrom, _bgTo; + QImage _iconMixedMask, _iconFrom, _iconTo, _iconMixed; + +}; + +GroupPanel::Button::Button( + QWidget *parent, + const style::CallButton &stFrom, + const style::CallButton *stTo) +: Ui::RippleButton(parent, stFrom.button.ripple) +, _stFrom(&stFrom) +, _stTo(stTo) { + resize(_stFrom->button.width, _stFrom->button.height); + + _bgMask = prepareRippleMask(); + _bgFrom = App::pixmapFromImageInPlace(style::colorizeImage(_bgMask, _stFrom->bg)); + if (_stTo) { + Assert(_stFrom->button.width == _stTo->button.width); + Assert(_stFrom->button.height == _stTo->button.height); + Assert(_stFrom->button.rippleAreaPosition == _stTo->button.rippleAreaPosition); + Assert(_stFrom->button.rippleAreaSize == _stTo->button.rippleAreaSize); + + _bg = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _bg.setDevicePixelRatio(cRetinaFactor()); + _bgTo = App::pixmapFromImageInPlace(style::colorizeImage(_bgMask, _stTo->bg)); + _iconMixedMask = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconMixedMask.setDevicePixelRatio(cRetinaFactor()); + _iconFrom = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconFrom.setDevicePixelRatio(cRetinaFactor()); + _iconFrom.fill(Qt::black); + { + Painter p(&_iconFrom); + p.drawImage((_stFrom->button.rippleAreaSize - _stFrom->button.icon.width()) / 2, (_stFrom->button.rippleAreaSize - _stFrom->button.icon.height()) / 2, _stFrom->button.icon.instance(Qt::white)); + } + _iconTo = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconTo.setDevicePixelRatio(cRetinaFactor()); + _iconTo.fill(Qt::black); + { + Painter p(&_iconTo); + p.drawImage((_stTo->button.rippleAreaSize - _stTo->button.icon.width()) / 2, (_stTo->button.rippleAreaSize - _stTo->button.icon.height()) / 2, _stTo->button.icon.instance(Qt::white)); + } + _iconMixed = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied); + _iconMixed.setDevicePixelRatio(cRetinaFactor()); + } +} + +void GroupPanel::Button::setText(rpl::producer text) { + _label.create(this, std::move(text), _stFrom->label); + _label->show(); + rpl::combine( + sizeValue(), + _label->sizeValue() + ) | rpl::start_with_next([=](QSize my, QSize label) { + _label->moveToLeft( + (my.width() - label.width()) / 2, + my.height() - label.height(), + my.width()); + }, _label->lifetime()); +} + +void GroupPanel::Button::setProgress(float64 progress) { + _progress = progress; + update(); +} + +void GroupPanel::Button::paintEvent(QPaintEvent *e) { + Painter p(this); + + auto bgPosition = myrtlpoint(_stFrom->button.rippleAreaPosition); + auto paintFrom = (_progress == 0.) || !_stTo; + auto paintTo = !paintFrom && (_progress == 1.); + + if (paintFrom) { + p.drawPixmap(bgPosition, _bgFrom); + } else if (paintTo) { + p.drawPixmap(bgPosition, _bgTo); + } else { + style::colorizeImage(_bgMask, anim::color(_stFrom->bg, _stTo->bg, _progress), &_bg); + p.drawImage(bgPosition, _bg); + } + + auto rippleColorInterpolated = QColor(); + auto rippleColorOverride = &rippleColorInterpolated; + if (paintFrom) { + rippleColorOverride = nullptr; + } else if (paintTo) { + rippleColorOverride = &_stTo->button.ripple.color->c; + } else { + rippleColorInterpolated = anim::color(_stFrom->button.ripple.color, _stTo->button.ripple.color, _progress); + } + paintRipple(p, _stFrom->button.rippleAreaPosition.x(), _stFrom->button.rippleAreaPosition.y(), rippleColorOverride); + + auto positionFrom = iconPosition(_stFrom); + if (paintFrom) { + const auto icon = &_stFrom->button.icon; + icon->paint(p, positionFrom, width()); + } else { + auto positionTo = iconPosition(_stTo); + if (paintTo) { + _stTo->button.icon.paint(p, positionTo, width()); + } else { + mixIconMasks(); + style::colorizeImage(_iconMixedMask, st::callIconFg->c, &_iconMixed); + p.drawImage(myrtlpoint(_stFrom->button.rippleAreaPosition), _iconMixed); + } + } +} + +QPoint GroupPanel::Button::iconPosition(not_null st) const { + auto result = st->button.iconPosition; + if (result.x() < 0) { + result.setX((width() - st->button.icon.width()) / 2); + } + if (result.y() < 0) { + result.setY((height() - st->button.icon.height()) / 2); + } + return result; +} + +void GroupPanel::Button::mixIconMasks() { + _iconMixedMask.fill(Qt::black); + + Painter p(&_iconMixedMask); + PainterHighQualityEnabler hq(p); + auto paintIconMask = [this, &p](const QImage &mask, float64 angle) { + auto skipFrom = _stFrom->button.rippleAreaSize / 2; + p.translate(skipFrom, skipFrom); + p.rotate(angle); + p.translate(-skipFrom, -skipFrom); + p.drawImage(0, 0, mask); + }; + p.save(); + paintIconMask(_iconFrom, (_stFrom->angle - _stTo->angle) * _progress); + p.restore(); + p.setOpacity(_progress); + paintIconMask(_iconTo, (_stTo->angle - _stFrom->angle) * (1. - _progress)); +} + +void GroupPanel::Button::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + auto over = isOver(); + auto wasOver = static_cast(was & StateFlag::Over); + if (over != wasOver) { + update(); + } +} + +QPoint GroupPanel::Button::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()) - _stFrom->button.rippleAreaPosition; +} + +QImage GroupPanel::Button::prepareRippleMask() const { + return Ui::RippleAnimation::ellipseMask(QSize(_stFrom->button.rippleAreaSize, _stFrom->button.rippleAreaSize)); +} + +GroupPanel::GroupPanel(not_null call) +: _call(call) +, _channel(call->channel()) +, _window(std::make_unique(Core::App().getModalParent())) +#ifdef Q_OS_WIN +, _controls(std::make_unique( + _window.get(), + st::callTitle, + [=](bool maximized) { toggleFullScreen(maximized); })) +#endif // Q_OS_WIN +, _settings(widget(), st::callCancel) +, _hangup(widget(), st::callHangup) +, _mute(widget(), st::callMicrophoneMute, &st::callMicrophoneUnmute) { + initWindow(); + initWidget(); + initControls(); + initLayout(); + showAndActivate(); +} + +GroupPanel::~GroupPanel() = default; + +void GroupPanel::showAndActivate() { + _window->raise(); + _window->setWindowState(_window->windowState() | Qt::WindowActive); + _window->activateWindow(); + _window->setFocus(); +} + +void GroupPanel::initWindow() { + _window->setAttribute(Qt::WA_OpaquePaintEvent); + _window->setAttribute(Qt::WA_NoSystemBackground); + _window->setWindowIcon( + QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); + _window->setTitle(u" "_q); + _window->setTitleStyle(st::callTitle); + + _window->events( + ) | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Close) { + handleClose(); + } else if (e->type() == QEvent::KeyPress) { + if ((static_cast(e.get())->key() == Qt::Key_Escape) + && _window->isFullScreen()) { + _window->showNormal(); + } + } + }, _window->lifetime()); + + _window->setBodyTitleArea([=](QPoint widgetPoint) { + using Flag = Ui::WindowTitleHitTestFlag; + if (!widget()->rect().contains(widgetPoint)) { + return Flag::None | Flag(0); + } +#ifdef Q_OS_WIN + if (_controls->geometry().contains(widgetPoint)) { + return Flag::None | Flag(0); + } +#endif // Q_OS_WIN + const auto inControls = false; + return inControls + ? Flag::None + : (Flag::Move | Flag::FullScreen); + }); + +#ifdef Q_OS_WIN + // On Windows we replace snap-to-top maximizing with fullscreen. + // + // We have to switch first to showNormal, so that showFullScreen + // will remember correct normal window geometry and next showNormal + // will show it instead of a moving maximized window. + // + // We have to do it in InvokeQueued, otherwise it still captures + // the maximized window geometry and saves it. + // + // I couldn't find a less glitchy way to do that *sigh*. + const auto object = _window->windowHandle(); + const auto signal = &QWindow::windowStateChanged; + QObject::connect(object, signal, [=](Qt::WindowState state) { + if (state == Qt::WindowMaximized) { + InvokeQueued(object, [=] { + _window->showNormal(); + _window->showFullScreen(); + }); + } + }); +#endif // Q_OS_WIN +} + +void GroupPanel::initWidget() { + widget()->setMouseTracking(true); + + widget()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + paint(clip); + }, widget()->lifetime()); + + widget()->sizeValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, widget()->lifetime()); +} + +void GroupPanel::initControls() { + _mute->setClickedCallback([=] { + if (_call) { + _call->setMuted(!_call->muted()); + } + }); + _hangup->setClickedCallback([=] { + if (_call) { + _call->hangup(); + } + }); + _settings->setClickedCallback([=] { + }); + initWithCall(_call); +} + +void GroupPanel::initWithCall(GroupCall *call) { + _callLifetime.destroy(); + _call = call; + if (!_call) { + return; + } + + _channel = _call->channel(); + + _settings->setText(tr::lng_menu_settings()); + _hangup->setText(tr::lng_box_leave()); + + _call->mutedValue( + ) | rpl::start_with_next([=](bool mute) { + _mute->setProgress(mute ? 1. : 0.); + _mute->setText(mute + ? tr::lng_call_unmute_audio() + : tr::lng_call_mute_audio()); + }, _callLifetime); + + _call->stateValue( + ) | rpl::start_with_next([=](State state) { + stateChanged(state); + }, _callLifetime); +} + +void GroupPanel::initLayout() { + initGeometry(); + +#ifdef Q_OS_WIN + _controls->raise(); +#endif // Q_OS_WIN +} + +void GroupPanel::showControls() { + Expects(_call != nullptr); + + widget()->showChildren(); +} + +void GroupPanel::closeBeforeDestroy() { + _window->close(); + initWithCall(nullptr); +} + +void GroupPanel::initGeometry() { + const auto center = Core::App().getPointForCallPanelCenter(); + const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight); + _window->setGeometry(rect.translated(center - rect.center())); + _window->setMinimumSize(rect.size()); + _window->show(); + updateControlsGeometry(); +} + +void GroupPanel::toggleFullScreen(bool fullscreen) { + if (fullscreen) { + _window->showFullScreen(); + } else { + _window->showNormal(); + } +} + +void GroupPanel::updateControlsGeometry() { + if (widget()->size().isEmpty()) { + return; + } + const auto top = widget()->height() - 2 * _mute->height(); + _mute->move((widget()->width() - _mute->width()) / 2, top); + _settings->moveToLeft(_settings->width(), top); + _hangup->moveToRight(_settings->width(), top); +} + +void GroupPanel::paint(QRect clip) { + Painter p(widget()); + + auto region = QRegion(clip); + for (const auto rect : region) { + p.fillRect(rect, st::callBgOpaque); + } +} + +void GroupPanel::handleClose() { + if (_call) { + _call->hangup(); + } +} + +not_null GroupPanel::widget() const { + return _window->body(); +} + +void GroupPanel::stateChanged(State state) { + Expects(_call != nullptr); + + if ((state != State::HangingUp) + && (state != State::Ended) + && (state != State::FailedHangingUp) + && (state != State::Failed)) { + } +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h new file mode 100644 index 000000000..cec1fc107 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -0,0 +1,96 @@ +/* +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 "base/weak_ptr.h" +#include "base/timer.h" +#include "base/object_ptr.h" +#include "calls/calls_group_call.h" +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" + +class Image; + +namespace Data { +class PhotoMedia; +class CloudImageView; +} // namespace Data + +namespace Ui { +class IconButton; +class FlatLabel; +template +class FadeWrap; +template +class PaddingWrap; +class Window; +namespace Platform { +class TitleControls; +} // namespace Platform +} // namespace Ui + +namespace style { +struct CallSignalBars; +struct CallBodyLayout; +} // namespace style + +namespace Calls { + +class Userpic; +class SignalBars; + +class GroupPanel final { +public: + GroupPanel(not_null call); + ~GroupPanel(); + + void showAndActivate(); + void closeBeforeDestroy(); + +private: + class Button; + using State = GroupCall::State; + + [[nodiscard]] not_null widget() const; + + void paint(QRect clip); + + void initWindow(); + void initWidget(); + void initControls(); + void initWithCall(GroupCall *call); + void initLayout(); + void initGeometry(); + + void handleClose(); + + void updateControlsGeometry(); + void stateChanged(State state); + void showControls(); + void startDurationUpdateTimer(crl::time currentDuration); + + void toggleFullScreen(bool fullscreen); + + GroupCall *_call = nullptr; + not_null _channel; + + const std::unique_ptr _window; + +#ifdef Q_OS_WIN + std::unique_ptr _controls; +#endif // Q_OS_WIN + + rpl::lifetime _callLifetime; + + object_ptr