/* 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