You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
12 KiB
328 lines
12 KiB
#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() { |
|
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'))); |
|
}
|
|
|