tdesktop/Telegram/SourceFiles/calls/calls_group_panel.cpp

498 lines
14 KiB
C++

/*
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 "calls/calls_group_members.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/window.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/layer_manager.h"
#include "boxes/confirm_box.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "base/event_filter.h"
#include "app.h"
#include "styles/style_calls.h"
#ifdef Q_OS_WIN
#include "ui/platform/win/ui_window_title_win.h"
#endif // Q_OS_WIN
#include <QtWidgets/QDesktopWidget>
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
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<QString> 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<const style::CallButton*> st) const;
void mixIconMasks();
not_null<const style::CallButton*> _stFrom;
const style::CallButton *_stTo = nullptr;
float64 _progress = 0.;
object_ptr<Ui::FlatLabel> _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<QString> 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<const style::CallButton*> 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<bool>(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<GroupCall*> call)
: _call(call)
, _channel(call->channel())
, _window(std::make_unique<Ui::Window>(Core::App().getModalParent()))
, _layerBg(std::make_unique<Ui::LayerManager>(_window->body()))
#ifdef Q_OS_WIN
, _controls(std::make_unique<Ui::Platform::TitleControls>(
_window.get(),
st::callTitle))
#endif // Q_OS_WIN
, _members(widget(), call)
, _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() {
if (_window->isHidden()) {
_window->show();
}
_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->setTitleStyle(st::callTitle);
_window->setTitle(computeTitleRect()
? u" "_q
: tr::lng_group_call_title(tr::now));
base::install_event_filter(_window.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close && handleClose()) {
e->ignore();
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
_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::Maximize);
});
}
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([=] {
_layerBg->showBox(Box<ConfirmBox>(
tr::lng_group_call_leave_sure(tr::now),
tr::lng_group_call_leave(tr::now),
[=] { 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);
_members->desiredHeightValue(
) | rpl::start_with_next([=] {
updateControlsGeometry();
}, _members->lifetime());
_members->toggleMuteRequests(
) | rpl::start_with_next([=](GroupMembers::MuteRequest request) {
if (_call) {
_call->toggleMute(request.user, request.mute);
}
}, _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();
}
int GroupPanel::computeMembersListTop() const {
#ifdef Q_OS_WIN
return st::callTitleButton.height + st::groupCallMembersMargin.top() / 2;
#elif defined Q_OS_MAC // Q_OS_WIN
return st::groupCallMembersMargin.top() * 2;
#else // Q_OS_WIN || Q_OS_MAC
return st::groupCallMembersMargin.top();
#endif // Q_OS_WIN || Q_OS_MAC
}
std::optional<QRect> GroupPanel::computeTitleRect() const {
#ifdef Q_OS_WIN
const auto controls = _controls->geometry();
return QRect(0, 0, controls.x(), controls.height());
#else // Q_OS_WIN
return std::nullopt;
#endif // Q_OS_WIN
}
void GroupPanel::updateControlsGeometry() {
if (widget()->size().isEmpty()) {
return;
}
const auto desiredHeight = _members->desiredHeight();
const auto membersWidthAvailable = widget()->width()
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidthMin = st::groupCallWidth
- st::groupCallMembersMargin.left()
- st::groupCallMembersMargin.right();
const auto membersWidth = std::clamp(
membersWidthAvailable,
membersWidthMin,
st::groupCallMembersWidthMax);
const auto muteTop = widget()->height() - 2 * _mute->height();
const auto buttonsTop = muteTop;
const auto membersTop = computeMembersListTop();
const auto availableHeight = buttonsTop
- membersTop
- st::groupCallMembersMargin.bottom();
_members->setGeometry(
(widget()->width() - membersWidth) / 2,
membersTop,
membersWidth,
std::min(desiredHeight, availableHeight));
_mute->move((widget()->width() - _mute->width()) / 2, muteTop);
_settings->moveToLeft(_settings->width(), buttonsTop);
_hangup->moveToRight(_settings->width(), buttonsTop);
refreshTitle();
}
void GroupPanel::refreshTitle() {
if (const auto titleRect = computeTitleRect()) {
if (!_title) {
_title.create(
widget(),
tr::lng_group_call_title(),
st::groupCallHeaderLabel);
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
_window->setTitle(u" "_q);
}
const auto best = _title->naturalWidth();
const auto from = (widget()->width() - best) / 2;
const auto top = (computeMembersListTop() - _title->height()) / 2;
const auto left = titleRect->x();
if (from >= left && from + best <= left + titleRect->width()) {
_title->resizeToWidth(best);
_title->moveToLeft(from, top);
} else if (titleRect->width() < best) {
_title->resizeToWidth(titleRect->width());
_title->moveToLeft(left, top);
} else if (from < left) {
_title->resizeToWidth(best);
_title->moveToLeft(left, top);
} else {
_title->resizeToWidth(best);
_title->moveToLeft(left + titleRect->width() - best, top);
}
} else if (_title) {
_title.destroy();
_window->setTitle(tr::lng_group_call_title(tr::now));
}
}
void GroupPanel::paint(QRect clip) {
Painter p(widget());
auto region = QRegion(clip);
for (const auto rect : region) {
p.fillRect(rect, st::groupCallBg);
}
}
bool GroupPanel::handleClose() {
if (_call) {
_window->hide();
return true;
}
return false;
}
not_null<Ui::RpWidget*> 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