mumble-voip_mumble/src/mumble/ManualPlugin.cpp

433 lines
12 KiB
C++

// Copyright 2016-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include <QtCore/QtCore>
#include <QtGui/QtGui>
#include <QtWidgets/QMessageBox>
#include "ManualPlugin.h"
#include "ui_ManualPlugin.h"
#include "Global.h"
#include <QPointer>
#include <cmath>
#include <float.h>
#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
#include "../../plugins/mumble_legacy_plugin.h"
static QPointer< Manual > mDlg = nullptr;
static bool bLinkable = false;
static bool bActive = true;
static int iAzimuth = 0;
static int iElevation = 0;
static const QString defaultContext = QString::fromLatin1("Mumble");
static const QString defaultIdentity = QString::fromLatin1("Agent47");
static struct {
float avatar_pos[3];
float avatar_front[3];
float avatar_top[3];
float camera_pos[3];
float camera_front[3];
float camera_top[3];
std::string context;
std::wstring identity;
} my = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, std::string(), std::wstring() };
Manual::Manual(QWidget *p) : QDialog(p) {
setupUi(this);
qgvPosition->viewport()->installEventFilter(this);
qgvPosition->scale(1.0f, 1.0f);
m_qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);
const float indicatorDiameter = 4.0f;
QPainterPath indicator;
// The center of the indicator's circle will represent the current position
indicator.addEllipse(QRectF(-indicatorDiameter / 2, -indicatorDiameter / 2, indicatorDiameter, indicatorDiameter));
// A line will indicate the indicator's orientation (azimuth)
indicator.moveTo(0, -indicatorDiameter / 2);
indicator.lineTo(0, -indicatorDiameter);
m_qgiPosition = m_qgsScene->addPath(indicator);
qgvPosition->setScene(m_qgsScene);
qgvPosition->fitInView(-5.0f, -5.0f, 10.0f, 10.0f, Qt::KeepAspectRatio);
qdsbX->setRange(-FLT_MAX, FLT_MAX);
qdsbY->setRange(-FLT_MAX, FLT_MAX);
qdsbZ->setRange(-FLT_MAX, FLT_MAX);
qdsbX->setValue(my.avatar_pos[0]);
qdsbY->setValue(my.avatar_pos[1]);
qdsbZ->setValue(my.avatar_pos[2]);
qpbActivated->setChecked(bActive);
qpbLinked->setChecked(bLinkable);
qsbAzimuth->setValue(iAzimuth);
qsbElevation->setValue(iElevation);
updateTopAndFront(iAzimuth, iElevation);
// Set context and identity to default values in order to
// a) make positional audio work out of the box (needs a context)
// b) make the user aware of what each field might contain
qleContext->setText(defaultContext);
qleIdentity->setText(defaultIdentity);
my.context = defaultContext.toStdString();
my.identity = defaultIdentity.toStdWString();
qsbSilentUserDisplaytime->setValue(Global::get().s.manualPlugin_silentUserDisplaytime);
updateLoopRunning.store(false);
}
void Manual::setSpeakerPositions(const QHash< unsigned int, Position2D > &positions) {
if (mDlg) {
QMetaObject::invokeMethod(mDlg, "on_speakerPositionUpdate", Qt::QueuedConnection,
Q_ARG(PositionMap, positions));
}
}
bool Manual::eventFilter(QObject *obj, QEvent *evt) {
if ((evt->type() == QEvent::MouseButtonPress) || (evt->type() == QEvent::MouseMove)) {
QMouseEvent *qme = dynamic_cast< QMouseEvent * >(evt);
if (qme) {
if (qme->buttons() & Qt::LeftButton) {
QPointF qpf = qgvPosition->mapToScene(qme->pos());
qdsbX->setValue(qpf.x());
qdsbZ->setValue(-qpf.y());
m_qgiPosition->setPos(qpf);
}
}
}
return QDialog::eventFilter(obj, evt);
}
void Manual::changeEvent(QEvent *e) {
QDialog::changeEvent(e);
switch (e->type()) {
case QEvent::LanguageChange:
retranslateUi(this);
break;
default:
break;
}
}
void Manual::on_qpbUnhinge_pressed() {
qpbUnhinge->setEnabled(false);
mDlg->setParent(nullptr);
mDlg->show();
}
void Manual::on_qpbLinked_clicked(bool b) {
bLinkable = b;
}
void Manual::on_qpbActivated_clicked(bool b) {
bActive = b;
}
void Manual::on_qdsbX_valueChanged(double d) {
my.avatar_pos[0] = my.camera_pos[0] = static_cast< float >(d);
m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qdsbY_valueChanged(double d) {
my.avatar_pos[1] = my.camera_pos[1] = static_cast< float >(d);
}
void Manual::on_qdsbZ_valueChanged(double d) {
my.avatar_pos[2] = my.camera_pos[2] = static_cast< float >(d);
m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qsbAzimuth_valueChanged(int i) {
if (i > 360)
qdAzimuth->setValue(i % 360);
else
qdAzimuth->setValue(i);
updateTopAndFront(i, qsbElevation->value());
}
void Manual::on_qsbElevation_valueChanged(int i) {
qdElevation->setValue(90 - i);
updateTopAndFront(qsbAzimuth->value(), i);
}
void Manual::on_qdAzimuth_valueChanged(int i) {
if (i < 0)
qsbAzimuth->setValue(360 + i);
else
qsbAzimuth->setValue(i);
}
void Manual::on_qdElevation_valueChanged(int i) {
if (i < -90)
qdElevation->setValue(180);
else if (i < 0)
qdElevation->setValue(0);
else
qsbElevation->setValue(90 - i);
}
void Manual::on_qleContext_editingFinished() {
my.context = qleContext->text().toStdString();
}
void Manual::on_qleIdentity_editingFinished() {
my.identity = qleIdentity->text().toStdWString();
}
void Manual::on_buttonBox_clicked(QAbstractButton *button) {
if (buttonBox->buttonRole(button) == buttonBox->ResetRole) {
qpbLinked->setChecked(false);
qpbActivated->setChecked(true);
bLinkable = false;
bActive = true;
qdsbX->setValue(0);
qdsbY->setValue(0);
qdsbZ->setValue(0);
qleContext->clear();
qleIdentity->clear();
qsbElevation->setValue(0);
qsbAzimuth->setValue(0);
}
}
void Manual::on_qsbSilentUserDisplaytime_valueChanged(int value) {
Global::get().s.manualPlugin_silentUserDisplaytime = value;
}
void Manual::on_speakerPositionUpdate(QHash< unsigned int, Position2D > positions) {
// First iterate over the stale items to check whether one of them is actually no longer stale
QMutableHashIterator< unsigned int, StaleEntry > staleIt(staleSpeakerPositions);
while (staleIt.hasNext()) {
staleIt.next();
const unsigned int sessionID = staleIt.key();
QGraphicsItem *staleItem = staleIt.value().staleItem;
if (positions.contains(sessionID)) {
// The item is no longer stale -> restore opacity and re-insert into speakerPositions
staleItem->setOpacity(1.0);
staleIt.remove();
speakerPositions.insert(sessionID, staleItem);
} else if (!updateLoopRunning.load()) {
QMetaObject::invokeMethod(this, "on_updateStaleSpeakers", Qt::QueuedConnection);
updateLoopRunning.store(true);
}
}
// Now iterate over all active items and check whether they have become stale or whether their
// position can be updated
QMutableHashIterator< unsigned int, QGraphicsItem * > speakerIt(speakerPositions);
while (speakerIt.hasNext()) {
speakerIt.next();
const unsigned int sessionID = speakerIt.key();
QGraphicsItem *speakerItem = speakerIt.value();
if (positions.contains(sessionID)) {
Position2D newPos = positions.take(sessionID);
// Update speaker's position (remember that y-axis is inverted in screen-coordinates
speakerItem->setPos(newPos.x, -newPos.y);
} else {
// Remove the stale item
speakerIt.remove();
if (Global::get().s.manualPlugin_silentUserDisplaytime == 0) {
// Delete it immediately
delete speakerItem;
} else {
staleSpeakerPositions.insert(sessionID, { std::chrono::steady_clock::now(), speakerItem });
}
}
}
// Finally iterate over the remaining new speakers and create new items for them
QHashIterator< unsigned int, Position2D > remainingIt(positions);
while (remainingIt.hasNext()) {
remainingIt.next();
const float speakerRadius = 1.2f;
QGraphicsItem *speakerItem = m_qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
2 * speakerRadius, QPen(), QBrush(Qt::red));
Position2D pos = remainingIt.value();
// y-axis is inverted in screen-space
speakerItem->setPos(pos.x, -pos.y);
speakerPositions.insert(remainingIt.key(), speakerItem);
}
}
void Manual::on_updateStaleSpeakers() {
if (staleSpeakerPositions.isEmpty()) {
// If there are no stale speakers, this loop doesn't have to run
updateLoopRunning.store(false);
return;
}
// Iterate over all stale items and check whether they have to be removed entirely. If not, update
// their opacity.
QMutableHashIterator< unsigned int, StaleEntry > staleIt(staleSpeakerPositions);
while (staleIt.hasNext()) {
staleIt.next();
StaleEntry entry = staleIt.value();
double elapsedTime =
static_cast< std::chrono::duration< double > >(std::chrono::steady_clock::now() - entry.staleSince).count();
if (elapsedTime >= Global::get().s.manualPlugin_silentUserDisplaytime) {
// The item has been around long enough - remove it now
staleIt.remove();
delete entry.staleItem;
} else {
// Let the item fade out
double opacity = (Global::get().s.manualPlugin_silentUserDisplaytime - elapsedTime)
/ static_cast< double >(Global::get().s.manualPlugin_silentUserDisplaytime);
entry.staleItem->setOpacity(opacity);
}
}
if (!staleSpeakerPositions.isEmpty()) {
updateLoopRunning.store(true);
// Call this function again in the next iteration of the event loop
QMetaObject::invokeMethod(this, "on_updateStaleSpeakers", Qt::QueuedConnection);
} else {
updateLoopRunning.store(false);
}
}
void Manual::updateTopAndFront(int azimuth, int elevation) {
iAzimuth = azimuth;
iElevation = elevation;
m_qgiPosition->setRotation(azimuth);
double azim = azimuth * M_PI / 180.;
double elev = elevation * M_PI / 180.;
my.avatar_front[0] = static_cast< float >(cos(elev) * sin(azim));
my.avatar_front[1] = static_cast< float >(sin(elev));
my.avatar_front[2] = static_cast< float >(cos(elev) * cos(azim));
my.avatar_top[0] = static_cast< float >(-sin(elev) * sin(azim));
my.avatar_top[1] = static_cast< float >(cos(elev));
my.avatar_top[2] = static_cast< float >(-sin(elev) * cos(azim));
memcpy(my.camera_top, my.avatar_top, sizeof(float) * 3);
memcpy(my.camera_front, my.avatar_front, sizeof(float) * 3);
}
static int trylock() {
return bLinkable;
}
static void unlock() {
if (mDlg) {
mDlg->qpbLinked->setChecked(false);
}
bLinkable = false;
}
static void config(void *ptr) {
QWidget *w = reinterpret_cast< QWidget * >(ptr);
if (mDlg) {
mDlg->setParent(w, Qt::Dialog);
mDlg->qpbUnhinge->setEnabled(true);
} else {
mDlg = new Manual(w);
}
mDlg->show();
}
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
if (!bLinkable)
return false;
if (!bActive) {
memset(avatar_pos, 0, sizeof(float) * 3);
memset(camera_pos, 0, sizeof(float) * 3);
return true;
}
memcpy(avatar_pos, my.avatar_pos, sizeof(float) * 3);
memcpy(avatar_front, my.avatar_front, sizeof(float) * 3);
memcpy(avatar_top, my.avatar_top, sizeof(float) * 3);
memcpy(camera_pos, my.camera_pos, sizeof(float) * 3);
memcpy(camera_front, my.camera_front, sizeof(float) * 3);
memcpy(camera_top, my.camera_top, sizeof(float) * 3);
context.assign(my.context);
identity.assign(my.identity);
return true;
}
static const std::wstring longdesc() {
return std::wstring(L"This is the manual placement plugin. It allows you to place yourself manually.");
}
static std::wstring description(L"Manual placement plugin");
static std::wstring shortname(L"Manual placement");
static void about(void *ptr) {
QWidget *w = reinterpret_cast< QWidget * >(ptr);
QMessageBox::about(w, QString::fromStdWString(description), QString::fromStdWString(longdesc()));
}
static MumblePlugin manual = { MUMBLE_PLUGIN_MAGIC,
description,
shortname,
nullptr, // About is handled by MumblePluginQt
nullptr, // Config is handled by MumblePluginQt
trylock,
unlock,
longdesc,
fetch };
static MumblePluginQt manualqt = { MUMBLE_PLUGIN_MAGIC_QT, about, config };
MumblePlugin *ManualPlugin_getMumblePlugin() {
return &manual;
}
MumblePluginQt *ManualPlugin_getMumblePluginQt() {
return &manualqt;
}
/////////// Implementation of the ManualPlugin class //////////////
ManualPlugin::ManualPlugin(QObject *p) : LegacyPlugin(QString::fromLatin1("manual.builtin"), true, p) {
}
ManualPlugin::~ManualPlugin() {
}
void ManualPlugin::resolveFunctionPointers() {
m_mumPlug = &manual;
m_mumPlugQt = &manualqt;
}