diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 15ddbea3..d74f160d 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -142,8 +144,8 @@ static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** c CallWindow::CallWindow(QWidget* parent) : QWidget(parent, Qt::Window) { setWindowTitle(QCoreApplication::translate("ChatGui", "Call")); - resize(640, 800); - setMinimumSize(400, 600); + resize(520, 520); + setMinimumSize(420, 360); setAttribute(Qt::WA_DeleteOnClose, false); m_avatarLabel = new QLabel(this); @@ -188,7 +190,12 @@ CallWindow::CallWindow(QWidget* parent) "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); - QVBoxLayout* lay = new QVBoxLayout(this); + auto* root = new QVBoxLayout(this); + root->setContentsMargins(12, 12, 12, 12); root->addStretch(1); + m_panel = new QWidget(this); m_panel->setObjectName("callPanel"); m_panel->setMaximumWidth(640); + m_panel->setStyleSheet("QWidget#callPanel { background: palette(window); border-radius: 12px; }"); + root->addWidget(m_panel, 0, Qt::AlignHCenter | Qt::AlignBottom); + QVBoxLayout* lay = new QVBoxLayout(m_panel); lay->setContentsMargins(16, 20, 16, 16); lay->setSpacing(8); lay->addWidget(m_avatarLabel, 0, Qt::AlignHCenter); @@ -199,12 +206,19 @@ CallWindow::CallWindow(QWidget* parent) lay->addWidget(m_bgpPathLabel); lay->addWidget(m_statsLabel); m_video = new CallVideoEngine(this); - m_videoImage = new QLabel(this); m_videoImage->setMinimumHeight(180); m_videoImage->setAlignment(Qt::AlignCenter); - lay->addWidget(m_videoImage, 1); + m_localPreview = new QLabel(this); m_localPreview->setAlignment(Qt::AlignCenter); m_localPreview->hide(); + lay->addWidget(m_localPreview, 0, Qt::AlignHCenter); m_videoStatus = new QLabel(this); m_videoStatus->setWordWrap(true); lay->addWidget(m_videoStatus); m_videoControls = new QWidget(this); auto* videoLayout = new QVBoxLayout(m_videoControls); videoLayout->setContentsMargins(0, 0, 0, 0); - m_videoEnabled = new QCheckBox(tr("Send video"), this); videoLayout->addWidget(m_videoEnabled); + auto* actions = new QHBoxLayout(); + m_videoEnabled = new QCheckBox(tr("Send video"), this); actions->addWidget(m_videoEnabled); + actions->addStretch(1); + m_settingsBtn = new QPushButton(tr("Call settings"), this); m_settingsBtn->setCheckable(true); actions->addWidget(m_settingsBtn); + m_fullScreenBtn = new QPushButton(tr("Full screen"), this); actions->addWidget(m_fullScreenBtn); + lay->addLayout(actions); + connect(m_settingsBtn, &QPushButton::toggled, this, [this] { updateVideoPresentation(); }); + connect(m_fullScreenBtn, &QPushButton::clicked, this, &CallWindow::toggleFullScreen); auto* modes = new QHBoxLayout(); m_camera = new QComboBox(this); for (const auto& camera : CallVideoEngine::cameras()) m_camera->addItem(camera.first, camera.second); @@ -224,9 +238,22 @@ CallWindow::CallWindow(QWidget* parent) }; connect(m_videoMin, &QSpinBox::editingFinished, this, updateRange); connect(m_videoMax, &QSpinBox::editingFinished, this, updateRange); - connect(m_video, &CallVideoEngine::statusChanged, m_videoStatus, &QLabel::setText); + connect(m_video, &CallVideoEngine::statusChanged, this, [this](const QString& text) { + m_videoStatus->setText(text); m_videoStatus->setVisible(m_state == Active && !text.isEmpty()); + }); connect(m_video, &CallVideoEngine::imageChanged, this, [this](const QImage& image) { - m_videoImage->setPixmap(image.isNull() ? QPixmap() : QPixmap::fromImage(image).scaled(m_videoImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + bool changed = m_remoteImage.isNull() != image.isNull(); m_remoteImage = image; + if (changed) updateVideoPresentation(); + update(); + }); + connect(m_video, &CallVideoEngine::localImageChanged, this, [this](const QImage& image) { + bool changed = m_localImage.isNull() != image.isNull(); m_localImage = image; + if (!image.isNull()) { + QSize size = image.size().scaled(200, 150, Qt::KeepAspectRatio); m_localPreview->setFixedSize(size); + m_localPreview->setPixmap(QPixmap::fromImage(image).scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } else m_localPreview->clear(); + if (changed) updateVideoPresentation(); + update(); }); auto* videoTimer = new QTimer(this); videoTimer->setInterval(100); connect(videoTimer, &QTimer::timeout, this, [this] { @@ -237,7 +264,6 @@ CallWindow::CallWindow(QWidget* parent) m_videoMin->setValue(status.minimum_bps / 1000); m_videoMax->setValue(status.maximum_bps / 1000); m_videoRangeReady = true; } }); videoTimer->start(); - lay->addStretch(1); m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept")); m_declineCell = wrapWithCaption(m_declineBtn, QCoreApplication::translate("ChatGui", "Decline")); @@ -253,8 +279,7 @@ CallWindow::CallWindow(QWidget* parent) btn->addWidget(m_muteCell); btn->addStretch(1); lay->addLayout(btn); - lay->addStretch(1); - lay->addWidget(m_deviceCombo); + videoLayout->addWidget(m_deviceCombo); connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept); connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline); @@ -278,7 +303,7 @@ void CallWindow::setPeerName(const QString& name) { void CallWindow::setBgpPath(const QString& path) { m_bgpPathLabel->setText(path); - m_bgpPathLabel->setVisible(!path.isEmpty()); + m_bgpPathLabel->setVisible(!path.isEmpty() && m_settingsBtn->isChecked()); } void CallWindow::setConnectionType(uint8_t type, quint64 nextHop) { @@ -344,10 +369,66 @@ void CallWindow::updateButtons() { m_declineCell->setVisible(m_state == Incoming); m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active); m_muteCell->setVisible(m_state == Active); - m_deviceCombo->setVisible(m_state == Active); - m_videoControls->setVisible(m_state == Active); - m_videoImage->setVisible(m_state == Active); - m_videoStatus->setVisible(m_state == Active); + updateVideoPresentation(); +} + +void CallWindow::leaveFullScreen() { + if (!isFullScreen()) return; + showNormal(); + if (!m_windowGeometry.isEmpty()) restoreGeometry(m_windowGeometry); +} + +void CallWindow::toggleFullScreen() { + if (m_remoteImage.isNull()) return; + if (isFullScreen()) leaveFullScreen(); + else { m_windowGeometry = saveGeometry(); showFullScreen(); } + m_fullScreenBtn->setText(isFullScreen() ? tr("Exit full screen") : tr("Full screen")); +} + +void CallWindow::updateVideoPresentation() { + bool receiving = m_state == Active && !m_remoteImage.isNull(); + if (receiving != m_receivingVideo) { + m_receivingVideo = receiving; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video presentation remote=%d local=%d", receiving, !m_localImage.isNull()); + if (receiving) { + m_settingsBtn->setChecked(false); m_windowGeometry = saveGeometry(); showFullScreen(); + } else leaveFullScreen(); + } + bool active = m_state == Active, settings = active && m_settingsBtn->isChecked(); + m_localPreview->setVisible(active && !receiving && !m_localImage.isNull()); + m_avatarLabel->setVisible(!receiving); m_statusLabel->setVisible(!receiving); + m_connectionLabel->setVisible(settings); m_bgpPathLabel->setVisible(settings && !m_bgpPathLabel->text().isEmpty()); + m_statsLabel->setVisible(settings && !m_statsLabel->text().isEmpty()); + m_videoEnabled->setVisible(active); m_settingsBtn->setVisible(active); + m_videoControls->setVisible(settings); m_fullScreenBtn->setVisible(receiving); + m_fullScreenBtn->setText(isFullScreen() ? tr("Exit full screen") : tr("Full screen")); + m_videoStatus->setVisible(active && !m_videoStatus->text().isEmpty()); + update(); +} + +void CallWindow::paintEvent(QPaintEvent* event) { + QWidget::paintEvent(event); + if (!m_receivingVideo) return; + QPainter painter(this); painter.fillRect(rect(), Qt::black); painter.setRenderHint(QPainter::SmoothPixmapTransform); + QSize remote = m_remoteImage.size().scaled(size(), Qt::KeepAspectRatio); + painter.drawImage(QRect(QPoint((width() - remote.width()) / 2, (height() - remote.height()) / 2), remote), m_remoteImage); + if (!m_localImage.isNull()) { + QSize preview = m_localImage.size().scaled(qMin(240, width() / 4), qMin(180, height() / 4), Qt::KeepAspectRatio); + QRect target(QPoint(width() - preview.width() - 20, 20), preview); + painter.setPen(QPen(Qt::white, 2)); painter.drawRect(target.adjusted(-1, -1, 1, 1)); + painter.drawImage(target, m_localImage); + } +} + +void CallWindow::keyPressEvent(QKeyEvent* event) { + if (event->key() == Qt::Key_Escape && isFullScreen()) { toggleFullScreen(); event->accept(); return; } + if (event->key() == Qt::Key_F11 && m_receivingVideo) { toggleFullScreen(); event->accept(); return; } + QWidget::keyPressEvent(event); +} + +void CallWindow::mouseDoubleClickEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton && m_receivingVideo) { toggleFullScreen(); event->accept(); return; } + QWidget::mouseDoubleClickEvent(event); } void CallWindow::applyVideo() { diff --git a/tools/chatgui/src/callwindow.h b/tools/chatgui/src/callwindow.h index 33c104f4..5c47f573 100644 --- a/tools/chatgui/src/callwindow.h +++ b/tools/chatgui/src/callwindow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include class QLabel; @@ -36,6 +37,9 @@ signals: protected: void closeEvent(QCloseEvent* event) override; + void paintEvent(QPaintEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; private slots: void onAccept(); @@ -50,12 +54,19 @@ private: void populateDeviceCombo(); void postCallFn(void (*fn)(void*), uint64_t callId); void applyVideo(); + void updateVideoPresentation(); + void toggleFullScreen(); + void leaveFullScreen(); CallVideoEngine* m_video; QCheckBox* m_videoEnabled; QComboBox *m_camera, *m_resolution, *m_codec; QSpinBox *m_videoMin, *m_videoMax; - QLabel *m_videoImage, *m_videoStatus; - QWidget* m_videoControls; + QLabel *m_localPreview, *m_videoStatus; + QWidget *m_videoControls, *m_panel; + QPushButton *m_settingsBtn, *m_fullScreenBtn; + QImage m_remoteImage, m_localImage; + QByteArray m_windowGeometry; + bool m_receivingVideo = false; quint64 m_videoCall = 0; bool m_videoRangeReady = false;