330 lines
12 KiB
C++
330 lines
12 KiB
C++
#include "callwindow.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include "call_audio_engine.h"
|
|
#include "sound_manager.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QComboBox>
|
|
#include <QTimer>
|
|
#include <QDateTime>
|
|
#include <QCloseEvent>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <cstring>
|
|
|
|
extern "C" {
|
|
#include "call/call.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/debug_config.h"
|
|
}
|
|
|
|
/* ── рисование иконок звонка (QPainter, без внешних ассетов) ── */
|
|
|
|
enum class Glyph { Phone, PhoneDown, Mic, MicOff };
|
|
|
|
static QColor colorForName(const QString& name) {
|
|
static const QColor palette[] = {
|
|
QColor("#5B9BD5"), QColor("#70AD47"), QColor("#ED7D31"),
|
|
QColor("#9B59B6"), QColor("#F4B400"), QColor("#E67E22"),
|
|
QColor("#16A085"), QColor("#D35400"),
|
|
};
|
|
quint32 h = 0;
|
|
for (const QChar c : name) h = h * 31u + c.unicode();
|
|
return palette[h % 8];
|
|
}
|
|
|
|
static QPixmap makeAvatar(const QString& name, int size) {
|
|
QPixmap pm(size, size);
|
|
pm.fill(Qt::transparent);
|
|
QPainter p(&pm);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
p.setBrush(colorForName(name));
|
|
p.setPen(Qt::NoPen);
|
|
p.drawEllipse(0, 0, size, size);
|
|
QString initial = name.trimmed().isEmpty() ? QStringLiteral("?") : name.trimmed().left(1).toUpper();
|
|
p.setPen(Qt::white);
|
|
QFont f = p.font();
|
|
f.setBold(true);
|
|
f.setPixelSize(size / 2);
|
|
p.setFont(f);
|
|
p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, initial);
|
|
p.end();
|
|
return pm;
|
|
}
|
|
|
|
static void drawPhoneGlyph(QPainter* p, const QRectF& r, bool down) {
|
|
const qreal s = r.width();
|
|
p->save();
|
|
p->translate(r.center());
|
|
p->rotate(down ? 135.0 : -45.0);
|
|
const qreal w = s * 0.36, h = s * 0.64;
|
|
QPainterPath path;
|
|
path.addRoundedRect(QRectF(-w / 2, -h / 2, w, h), w * 0.5, w * 0.5);
|
|
p->setPen(Qt::NoPen);
|
|
p->setBrush(Qt::white);
|
|
p->drawPath(path);
|
|
p->restore();
|
|
}
|
|
|
|
static void drawMicGlyph(QPainter* p, const QRectF& r, bool off) {
|
|
const qreal s = r.width();
|
|
const QPointF c = r.center();
|
|
p->save();
|
|
QPen pen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
|
|
p->setPen(pen);
|
|
p->setBrush(Qt::NoBrush);
|
|
const qreal cw = s * 0.30;
|
|
p->drawRoundedRect(QRectF(c.x() - cw / 2, r.top() + s * 0.10, cw, s * 0.40), cw / 2, cw / 2);
|
|
p->drawArc(QRectF(c.x() - s * 0.20, r.top() + s * 0.42, s * 0.40, s * 0.26), 180 * 16, 180 * 16);
|
|
p->drawLine(QPointF(c.x(), r.top() + s * 0.60), QPointF(c.x(), r.top() + s * 0.68));
|
|
p->drawLine(QPointF(c.x() - s * 0.17, r.top() + s * 0.68), QPointF(c.x() + s * 0.17, r.top() + s * 0.68));
|
|
if (off) {
|
|
p->setPen(QPen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap));
|
|
p->drawLine(QPointF(c.x() - s * 0.26, c.y() - s * 0.26), QPointF(c.x() + s * 0.26, c.y() + s * 0.26));
|
|
}
|
|
p->restore();
|
|
}
|
|
|
|
static QIcon makeRoundIcon(const QColor& bg, Glyph glyph, int size) {
|
|
QPixmap pm(size, size);
|
|
pm.fill(Qt::transparent);
|
|
QPainter p(&pm);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(bg);
|
|
p.drawEllipse(0, 0, size, size);
|
|
const QRectF glyphRect(size * 0.24, size * 0.24, size * 0.52, size * 0.52);
|
|
switch (glyph) {
|
|
case Glyph::Phone: drawPhoneGlyph(&p, glyphRect, false); break;
|
|
case Glyph::PhoneDown: drawPhoneGlyph(&p, glyphRect, true); break;
|
|
case Glyph::Mic: drawMicGlyph(&p, glyphRect, false); break;
|
|
case Glyph::MicOff: drawMicGlyph(&p, glyphRect, true); break;
|
|
}
|
|
p.end();
|
|
return QIcon(pm);
|
|
}
|
|
|
|
static QPushButton* makeRoundBtn(const QIcon& icon, const QString& tooltip, QWidget* parent) {
|
|
auto* btn = new QPushButton(parent);
|
|
btn->setIcon(icon);
|
|
btn->setIconSize(QSize(52, 52));
|
|
btn->setFixedSize(64, 64);
|
|
btn->setCursor(Qt::PointingHandCursor);
|
|
btn->setToolTip(tooltip);
|
|
btn->setStyleSheet(
|
|
"QPushButton { border: none; border-radius: 32px; background: transparent; }"
|
|
"QPushButton:pressed { background: rgba(255,255,255,0.15); }");
|
|
return btn;
|
|
}
|
|
|
|
static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** captionOut = nullptr) {
|
|
auto* w = new QWidget(btn->parentWidget());
|
|
auto* l = new QVBoxLayout(w);
|
|
l->setContentsMargins(0, 0, 0, 0);
|
|
l->setSpacing(6);
|
|
l->addWidget(btn, 0, Qt::AlignHCenter);
|
|
auto* cap = new QLabel(caption, w);
|
|
cap->setAlignment(Qt::AlignCenter);
|
|
cap->setStyleSheet("color: palette(text); font-size: 11px; background: transparent;");
|
|
l->addWidget(cap, 0, Qt::AlignHCenter);
|
|
if (captionOut) *captionOut = cap;
|
|
return w;
|
|
}
|
|
|
|
CallWindow::CallWindow(QWidget* parent)
|
|
: QWidget(parent, Qt::Window) {
|
|
setWindowTitle(QStringLiteral("Звонок"));
|
|
setFixedSize(340, 400);
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
m_avatarLabel = new QLabel(this);
|
|
m_avatarLabel->setFixedSize(80, 80);
|
|
m_avatarLabel->setAlignment(Qt::AlignCenter);
|
|
|
|
m_peerLabel = new QLabel(this);
|
|
m_peerLabel->setAlignment(Qt::AlignCenter);
|
|
m_peerLabel->setStyleSheet("font-size: 17px; font-weight: bold; color: palette(text);");
|
|
|
|
m_statusLabel = new QLabel(this);
|
|
m_statusLabel->setAlignment(Qt::AlignCenter);
|
|
m_statusLabel->setStyleSheet("color: gray; font-size: 13px;");
|
|
|
|
m_durationLabel = new QLabel(QStringLiteral("00:00"), this);
|
|
m_durationLabel->setAlignment(Qt::AlignCenter);
|
|
m_durationLabel->setStyleSheet("font-size: 15px; color: palette(text); font-family: monospace;");
|
|
|
|
m_bgpPathLabel = new QLabel(this);
|
|
m_bgpPathLabel->setAlignment(Qt::AlignCenter);
|
|
m_bgpPathLabel->setWordWrap(true);
|
|
m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;");
|
|
m_bgpPathLabel->hide();
|
|
|
|
m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QStringLiteral("Принять"), this);
|
|
m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Отклонить"), this);
|
|
m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Завершить"), this);
|
|
m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QStringLiteral("Mute"), this);
|
|
m_muteBtn->setCheckable(true);
|
|
|
|
m_deviceCombo = new QComboBox(this);
|
|
m_deviceCombo->setStyleSheet(
|
|
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
|
|
" padding: 6px 10px; font-size: 13px; background: palette(base); }");
|
|
|
|
QVBoxLayout* lay = new QVBoxLayout(this);
|
|
lay->setContentsMargins(16, 20, 16, 16);
|
|
lay->setSpacing(8);
|
|
lay->addWidget(m_avatarLabel, 0, Qt::AlignHCenter);
|
|
lay->addWidget(m_peerLabel);
|
|
lay->addWidget(m_statusLabel);
|
|
lay->addWidget(m_durationLabel);
|
|
lay->addWidget(m_bgpPathLabel);
|
|
lay->addStretch(1);
|
|
|
|
m_acceptCell = wrapWithCaption(m_acceptBtn, QStringLiteral("Принять"));
|
|
m_declineCell = wrapWithCaption(m_declineBtn, QStringLiteral("Отклонить"));
|
|
m_hangupCell = wrapWithCaption(m_hangupBtn, QStringLiteral("Завершить"));
|
|
m_muteCell = wrapWithCaption(m_muteBtn, QStringLiteral("Mute"), &m_muteCaption);
|
|
|
|
QHBoxLayout* btn = new QHBoxLayout();
|
|
btn->setSpacing(10);
|
|
btn->addStretch(1);
|
|
btn->addWidget(m_acceptCell);
|
|
btn->addWidget(m_declineCell);
|
|
btn->addWidget(m_hangupCell);
|
|
btn->addWidget(m_muteCell);
|
|
btn->addStretch(1);
|
|
lay->addLayout(btn);
|
|
lay->addStretch(1);
|
|
lay->addWidget(m_deviceCombo);
|
|
|
|
connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept);
|
|
connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline);
|
|
connect(m_hangupBtn, &QPushButton::clicked, this, &CallWindow::onHangup);
|
|
connect(m_muteBtn, &QPushButton::toggled, this, &CallWindow::onMuteToggled);
|
|
connect(m_deviceCombo, QOverload<int>::of(&QComboBox::activated), this, &CallWindow::onDeviceChanged);
|
|
|
|
m_durationTimer = new QTimer(this);
|
|
m_durationTimer->setInterval(1000);
|
|
connect(m_durationTimer, &QTimer::timeout, this, &CallWindow::updateDuration);
|
|
|
|
setState(Outgoing);
|
|
}
|
|
|
|
CallWindow::~CallWindow() = default;
|
|
|
|
void CallWindow::setPeerName(const QString& name) {
|
|
m_peerLabel->setText(name);
|
|
m_avatarLabel->setPixmap(makeAvatar(name, 80));
|
|
}
|
|
|
|
void CallWindow::setBgpPath(const QString& path) {
|
|
m_bgpPathLabel->setText(path);
|
|
m_bgpPathLabel->setVisible(!path.isEmpty());
|
|
}
|
|
|
|
void CallWindow::setState(State s) {
|
|
m_state = s;
|
|
switch (s) {
|
|
case Outgoing:
|
|
m_statusLabel->setText(QStringLiteral("Звоним…"));
|
|
m_durationLabel->hide();
|
|
break;
|
|
case Incoming:
|
|
m_statusLabel->setText(QStringLiteral("Входящий звонок…"));
|
|
m_durationLabel->hide();
|
|
break;
|
|
case Active:
|
|
m_statusLabel->setText(QStringLiteral("Разговор"));
|
|
m_durationLabel->setText(QStringLiteral("00:00"));
|
|
m_durationLabel->show();
|
|
m_startMs = QDateTime::currentMSecsSinceEpoch();
|
|
m_durationTimer->start();
|
|
populateDeviceCombo();
|
|
m_muteBtn->setChecked(false);
|
|
break;
|
|
case Ended:
|
|
m_statusLabel->setText(QStringLiteral("Завершён"));
|
|
m_durationTimer->stop();
|
|
break;
|
|
}
|
|
updateButtons();
|
|
}
|
|
|
|
void CallWindow::populateDeviceCombo() {
|
|
m_deviceCombo->blockSignals(true);
|
|
m_deviceCombo->clear();
|
|
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1);
|
|
const auto devs = SoundManager::enumeratePlaybackDevices();
|
|
for (int i = 0; i < devs.size(); i++) {
|
|
QString label = devs[i].name;
|
|
if (devs[i].isDefault) label += QStringLiteral(" (default)");
|
|
m_deviceCombo->addItem(label, i);
|
|
}
|
|
int cur = CallAudioEngine::instance()->playbackDeviceIndex();
|
|
for (int i = 0; i < m_deviceCombo->count(); i++) {
|
|
if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
|
|
}
|
|
m_deviceCombo->blockSignals(false);
|
|
}
|
|
|
|
void CallWindow::updateButtons() {
|
|
m_acceptCell->setVisible(m_state == Incoming);
|
|
m_declineCell->setVisible(m_state == Incoming);
|
|
m_hangupCell->setVisible(m_state == Outgoing || m_state == Active);
|
|
m_muteCell->setVisible(m_state == Active);
|
|
m_deviceCombo->setVisible(m_state == Active);
|
|
}
|
|
|
|
void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) {
|
|
struct call_id_arg* a = (struct call_id_arg*)u_malloc(sizeof(*a));
|
|
if (!a) return;
|
|
a->inst = gui_bridge_get_inst();
|
|
a->call_id = callId;
|
|
gui_bridge_post_uasync_fn(fn, a);
|
|
}
|
|
|
|
void CallWindow::onAccept() {
|
|
SoundManager::instance()->stopLoop();
|
|
setState(Active);
|
|
postCallFn(call_accept_trampoline, m_callId);
|
|
}
|
|
|
|
void CallWindow::onDecline() {
|
|
postCallFn(call_decline_trampoline, m_callId);
|
|
}
|
|
|
|
void CallWindow::onHangup() {
|
|
postCallFn(call_hangup_trampoline, m_callId);
|
|
}
|
|
|
|
void CallWindow::onMuteToggled(bool checked) {
|
|
CallAudioEngine::instance()->setMuted(checked);
|
|
m_muteBtn->setIcon(makeRoundIcon(checked ? QColor("#e74c3c") : QColor("#7f8c8d"),
|
|
checked ? Glyph::MicOff : Glyph::Mic, 96));
|
|
if (m_muteCaption)
|
|
m_muteCaption->setText(checked ? QStringLiteral("Вкл. микрофон") : QStringLiteral("Mute"));
|
|
}
|
|
|
|
void CallWindow::onDeviceChanged(int index) {
|
|
int devIdx = m_deviceCombo->itemData(index).toInt();
|
|
CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx);
|
|
}
|
|
|
|
void CallWindow::closeEvent(QCloseEvent* event) {
|
|
if (m_state == Active) postCallFn(call_hangup_trampoline, m_callId);
|
|
emit closed(m_callId);
|
|
event->accept();
|
|
}
|
|
|
|
void CallWindow::updateDuration() {
|
|
if (m_state != Active) return;
|
|
qint64 ms = QDateTime::currentMSecsSinceEpoch() - m_startMs;
|
|
qint64 s = ms / 1000;
|
|
m_durationLabel->setText(QStringLiteral("%1:%2")
|
|
.arg(s / 60, 2, 10, QChar('0'))
|
|
.arg(s % 60, 2, 10, QChar('0')));
|
|
}
|