Add OSC action

This commit is contained in:
WarmUpTill 2023-05-02 23:10:59 +02:00 committed by WarmUpTill
parent b71f633fac
commit a1b5fbb1fe
8 changed files with 1301 additions and 4 deletions

View File

@ -107,6 +107,8 @@ target_sources(
src/macro-core/macro-action-macro.hpp
src/macro-core/macro-action-media.cpp
src/macro-core/macro-action-media.hpp
src/macro-core/macro-action-osc.cpp
src/macro-core/macro-action-osc.hpp
src/macro-core/macro-action-plugin-state.cpp
src/macro-core/macro-action-plugin-state.hpp
src/macro-core/macro-action-profile.cpp
@ -266,6 +268,8 @@ target_sources(
src/utils/obs-dock.hpp
src/utils/obs-module-helper.cpp
src/utils/obs-module-helper.hpp
src/utils/osc-helpers.cpp
src/utils/osc-helpers.hpp
src/utils/priority-helper.cpp
src/utils/priority-helper.hpp
src/utils/process-config.cpp

View File

@ -673,6 +673,7 @@ AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="Send message to {{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on {{listenDevices}}: {{listenButton}}"
AdvSceneSwitcher.action.osc="Open Sound Control"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
@ -1013,6 +1014,21 @@ AdvSceneSwitcher.midi.stopListen="Stop listening"
AdvSceneSwitcher.midi.startListenFail="Device is busy!\nSomething else is already listening!"
AdvSceneSwitcher.midi.deviceOpenFail="Failed to initialize MIDI device!"
AdvSceneSwitcher.osc.network="Network"
AdvSceneSwitcher.osc.network.protocol="Protocol:"
AdvSceneSwitcher.osc.network.address="Address:"
AdvSceneSwitcher.osc.network.port="Port:"
AdvSceneSwitcher.osc.message="Message"
AdvSceneSwitcher.osc.message.type.none="None"
AdvSceneSwitcher.osc.message.type.float="Float"
AdvSceneSwitcher.osc.message.type.int="Integer"
AdvSceneSwitcher.osc.message.type.string="String"
AdvSceneSwitcher.osc.message.type.binaryBlob="Binary blob"
AdvSceneSwitcher.osc.message.type.true="True"
AdvSceneSwitcher.osc.message.type.false="False"
AdvSceneSwitcher.osc.message.type.infinity="Infinitum"
AdvSceneSwitcher.osc.message.type.null="Nil"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"

View File

@ -0,0 +1,318 @@
#include "macro-action-osc.hpp"
#include "utility.hpp"
#include <system_error>
#include <QGroupBox>
namespace advss {
const std::string MacroActionOSC::id = "osc";
bool MacroActionOSC::_registered = MacroActionFactory::Register(
MacroActionOSC::id, {MacroActionOSC::Create, MacroActionOSCEdit::Create,
"AdvSceneSwitcher.action.osc"});
MacroActionOSC::MacroActionOSC(Macro *m)
: MacroAction(m),
_ctx(asio::io_context()),
_tcpSocket(asio::ip::tcp::socket(_ctx)),
_udpSocket(asio::ip::udp::socket(_ctx))
{
}
void MacroActionOSC::SendOSCTCPMessage(const asio::mutable_buffer &buffer)
{
try {
asio::write(_tcpSocket, asio::buffer(buffer));
} catch (const std::exception &e) {
blog(LOG_WARNING,
"failed to send OSC message \"%s\" via TCP %s %d: %s",
_message.ToString().c_str(), _ip.c_str(), _port.GetValue(),
e.what());
}
}
void MacroActionOSC::SendOSCUDPMessage(const asio::mutable_buffer &buffer)
{
try {
_udpSocket.send_to(buffer, _updEndpoint);
} catch (const std::exception &e) {
blog(LOG_WARNING,
"failed to send OSC message \"%s\" via UDP %s %d: %s",
_message.ToString().c_str(), _ip.c_str(), _port.GetValue(),
e.what());
}
}
void MacroActionOSC::UDPReconnect()
{
asio::error_code ec;
asio::ip::udp::resolver resolver(_ctx);
auto endpoints = resolver.resolve(asio::ip::udp::v4(), _ip.c_str(),
std::to_string(_port.GetValue()), ec);
if (ec) {
endpoints = resolver.resolve(asio::ip::udp::v6(), _ip.c_str(),
std::to_string(_port.GetValue()),
ec);
}
if (ec) {
blog(LOG_WARNING, "failed to get IP for \"%s\": %s",
_ip.c_str(), ec.message().c_str());
return;
}
try {
_updEndpoint = endpoints.begin()->endpoint();
_udpSocket = asio::ip::udp::socket(_ctx);
_udpSocket.open(endpoints.begin()->endpoint().protocol());
} catch (const std::exception &e) {
blog(LOG_WARNING, "failed to connect to UDP %s %d: %s",
_ip.c_str(), _port.GetValue(), e.what());
}
}
void MacroActionOSC::TCPReconnect()
{
asio::error_code ec;
asio::ip::tcp::resolver resolver(_ctx);
auto endpoints = resolver.resolve(asio::ip::tcp::v4(), _ip.c_str(),
std::to_string(_port.GetValue()), ec);
if (ec) {
endpoints = resolver.resolve(asio::ip::tcp::v6(), _ip.c_str(),
std::to_string(_port.GetValue()),
ec);
}
if (ec) {
blog(LOG_WARNING, "failed to get IP for \"%s\": %s",
_ip.c_str(), ec.message().c_str());
return;
}
try {
_tcpSocket = asio::ip::tcp::socket(_ctx);
_tcpSocket.connect(endpoints.begin()->endpoint());
} catch (const std::exception &e) {
blog(LOG_WARNING, "failed to connect to TCP %s %d: %s",
_ip.c_str(), _port.GetValue(), e.what());
}
}
void MacroActionOSC::CheckReconnect()
{
if (_protocol == Protocol::TCP &&
(_reconnect || !_tcpSocket.is_open())) {
TCPReconnect();
}
if (_protocol == Protocol::UDP &&
(_reconnect || !_udpSocket.is_open())) {
UDPReconnect();
}
}
bool MacroActionOSC::PerformAction()
{
auto buffer = _message.GetBuffer();
if (!buffer.has_value()) {
blog(LOG_WARNING, "failed to create or fill OSC buffer!");
return true;
}
CheckReconnect();
if (_protocol == Protocol::TCP &&
(_reconnect || !_tcpSocket.is_open())) {
TCPReconnect();
}
if (_protocol == Protocol::UDP &&
(_reconnect || !_udpSocket.is_open())) {
UDPReconnect();
}
auto rawMessage = asio::buffer(*buffer);
switch (_protocol) {
case MacroActionOSC::Protocol::TCP:
SendOSCTCPMessage(rawMessage);
break;
case MacroActionOSC::Protocol::UDP:
SendOSCUDPMessage(rawMessage);
break;
default:
break;
}
return true;
}
void MacroActionOSC::LogAction() const
{
vblog(LOG_INFO, "sending OSC message '%s' to %s %s %d",
_message.ToString().c_str(),
_protocol == Protocol::UDP ? "UDP" : "TCP", _ip.c_str(),
_port.GetValue());
}
bool MacroActionOSC::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "protocol", static_cast<int>(_protocol));
_ip.Save(obj, "ip");
_port.Save(obj, "port");
_message.Save(obj);
return true;
}
bool MacroActionOSC::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_protocol = static_cast<Protocol>(obs_data_get_int(obj, "protocol"));
_ip.Load(obj, "ip");
_port.Load(obj, "port");
_message.Load(obj);
return true;
}
void MacroActionOSC::SetProtocol(Protocol p)
{
_protocol = p;
_reconnect = true;
}
void MacroActionOSC::SetIP(const std::string &ip)
{
_ip = ip;
_reconnect = true;
}
void MacroActionOSC::SetPortNr(IntVariable port)
{
_port = port;
_reconnect = true;
}
static void populateProtocolSelection(QComboBox *list)
{
list->addItem("TCP");
list->addItem("UDP");
}
MacroActionOSCEdit::MacroActionOSCEdit(
QWidget *parent, std::shared_ptr<MacroActionOSC> entryData)
: QWidget(parent),
_protocol(new QComboBox(this)),
_ip(new VariableLineEdit(this)),
_port(new VariableSpinBox(this)),
_message(new OSCMessageEdit(this))
{
populateProtocolSelection(_protocol);
_port->setMaximum(65535);
auto networkGroup =
new QGroupBox(obs_module_text("AdvSceneSwitcher.osc.network"));
auto networkLayout = new QGridLayout;
int row = 0;
networkLayout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.osc.network.protocol")),
row, 0);
networkLayout->addWidget(_protocol, row, 1);
++row;
networkLayout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.osc.network.address")),
row, 0);
networkLayout->addWidget(_ip, row, 1);
++row;
networkLayout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.osc.network.port")),
row, 0);
networkLayout->addWidget(_port, row, 1);
networkGroup->setLayout(networkLayout);
auto messageGroup =
new QGroupBox(obs_module_text("AdvSceneSwitcher.osc.message"));
auto messageLayout = new QHBoxLayout();
messageLayout->addWidget(_message);
messageGroup->setLayout(messageLayout);
auto mainLayout = new QVBoxLayout;
mainLayout->addWidget(networkGroup);
mainLayout->addWidget(messageGroup);
setLayout(mainLayout);
QWidget::connect(_ip, SIGNAL(editingFinished()), this,
SLOT(IpChanged()));
QWidget::connect(_protocol, SIGNAL(currentIndexChanged(int)), this,
SLOT(ProtocolChanged(int)));
QWidget::connect(
_port,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(PortChanged(const NumberVariable<int> &)));
QWidget::connect(_message, SIGNAL(MessageChanged(const OSCMessage &)),
this, SLOT(MessageChanged(const OSCMessage &)));
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionOSCEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_protocol->setCurrentIndex(static_cast<int>(_entryData->GetProtocol()));
_ip->setText(_entryData->GetIP());
_port->SetValue(_entryData->GetPortNr());
_message->SetMessage(_entryData->_message);
adjustSize();
updateGeometry();
}
void MacroActionOSCEdit::IpChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->SetIP(_ip->text().toStdString());
}
void MacroActionOSCEdit::ProtocolChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->SetProtocol(static_cast<MacroActionOSC::Protocol>(value));
}
void MacroActionOSCEdit::PortChanged(const NumberVariable<int> &value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->SetPortNr(value);
}
void MacroActionOSCEdit::MessageChanged(const OSCMessage &m)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_message = m;
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@ -0,0 +1,94 @@
#pragma once
#include "macro-action-edit.hpp"
#include "osc-helpers.hpp"
#include <memory>
#include <asio.hpp>
namespace advss {
class MacroActionOSC : public MacroAction {
public:
MacroActionOSC(Macro *m);
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m)
{
return std::make_shared<MacroActionOSC>(m);
}
enum class Protocol {
TCP,
UDP,
};
void SetProtocol(Protocol);
Protocol GetProtocol() const { return _protocol; }
void SetIP(const std::string &);
StringVariable GetIP() const { return _ip; }
void SetPortNr(IntVariable);
IntVariable GetPortNr() { return _port; }
OSCMessage _message;
private:
void SendOSCTCPMessage(const asio::mutable_buffer &);
void SendOSCUDPMessage(const asio::mutable_buffer &);
void CheckReconnect();
void TCPReconnect();
void UDPReconnect();
Protocol _protocol = Protocol::UDP;
StringVariable _ip = "localhost";
IntVariable _port = 12345;
bool _reconnect = true;
asio::io_context _ctx;
asio::ip::tcp::socket _tcpSocket;
asio::ip::udp::socket _udpSocket;
asio::ip::udp::endpoint _updEndpoint;
static bool _registered;
static const std::string id;
};
class MacroActionOSCEdit : public QWidget {
Q_OBJECT
public:
MacroActionOSCEdit(QWidget *parent,
std::shared_ptr<MacroActionOSC> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionOSCEdit(
parent,
std::dynamic_pointer_cast<MacroActionOSC>(action));
}
private slots:
void IpChanged();
void MessageChanged(const OSCMessage &);
void ProtocolChanged(int);
void PortChanged(const NumberVariable<int> &value);
signals:
void HeaderInfoChanged(const QString &);
protected:
std::shared_ptr<MacroActionOSC> _entryData;
private:
QComboBox *_protocol;
VariableLineEdit *_ip;
VariableSpinBox *_port;
OSCMessageEdit *_message;
bool _loading = true;
};
} // namespace advss

View File

@ -1,4 +1,5 @@
#include "mouse-wheel-guard.hpp"
#include "osc-helpers.hpp"
#include <QEvent>
#include <QScrollBar>
@ -23,12 +24,15 @@ bool MouseWheelWidgetAdjustmentGuard::eventFilter(QObject *o, QEvent *e)
void PreventMouseWheelAdjustWithoutFocus(QWidget *w)
{
w->setFocusPolicy(Qt::StrongFocus);
// Ignore QScrollBar as there is no danger of accidentally modifying anything
// and long expanded QComboBox would be difficult to interact with otherwise.
if (qobject_cast<QScrollBar *>(w)) {
// Ignore OSCMessageElementEdit to allow OSCMessageEdit list to up update
// current index correctly.
if (qobject_cast<QScrollBar *>(w) ||
qobject_cast<OSCMessageElementEdit *>(w)) {
return;
}
w->setFocusPolicy(Qt::StrongFocus);
w->installEventFilter(new MouseWheelWidgetAdjustmentGuard(w));
}

689
src/utils/osc-helpers.cpp Normal file
View File

@ -0,0 +1,689 @@
#include "osc-helpers.hpp"
#include "obs-module-helper.hpp"
#include "utility.hpp"
#include <string.h>
#include <QGroupBox>
namespace advss {
std::unordered_map<size_t, OSCMessageElement::TypeInfo>
OSCMessageElement::_typeNames = {
{std::variant_npos,
{"AdvSceneSwitcher.osc.message.type.none", "-"}},
{0, {"AdvSceneSwitcher.osc.message.type.int", "i"}},
{1, {"AdvSceneSwitcher.osc.message.type.float", "f"}},
{2, {"AdvSceneSwitcher.osc.message.type.string", "s"}},
{3, {"AdvSceneSwitcher.osc.message.type.binaryBlob", "b"}},
{4, {"AdvSceneSwitcher.osc.message.type.true", "T"}},
{5, {"AdvSceneSwitcher.osc.message.type.false", "F"}},
{6, {"AdvSceneSwitcher.osc.message.type.infinity", "I"}},
{7, {"AdvSceneSwitcher.osc.message.type.null", "N"}},
};
// Based on https://github.com/mhroth/tinyosc
struct FillMessageElementBufferVisitor {
std::vector<char> &buffer;
uint32_t &curOffset;
bool success = false;
void operator()(const StringVariable &value)
{
std::string string = value;
int length = (int)strlen(string.c_str());
if (curOffset + length >= buffer.size()) {
buffer.resize(curOffset + length + 1);
}
strncpy(buffer.data() + curOffset, string.c_str(),
buffer.size() - curOffset - length);
curOffset = (curOffset + 4 + length) & ~0x3;
success = true;
}
void operator()(const IntVariable &value)
{
if (curOffset + 4 > buffer.size()) {
buffer.resize(curOffset + 4);
}
int32_t k = value;
*((uint32_t *)(buffer.data() + curOffset)) = htonl(k);
curOffset += 4;
success = true;
}
void operator()(const DoubleVariable &value)
{
if (curOffset + 4 > buffer.size()) {
buffer.resize(curOffset + 4);
}
const float f = value;
*((uint32_t *)(buffer.data() + curOffset)) =
htonl(*((uint32_t *)&f));
curOffset += 4;
success = true;
}
void operator()(const OSCBlob &value)
{
if (curOffset + 4 > buffer.size()) {
buffer.resize(curOffset + 4);
}
auto blob = value.GetBinary();
if (!blob.has_value()) {
return;
}
if (curOffset + 4 + blob->size() > buffer.size()) {
buffer.resize(curOffset + 4 + blob->size());
}
*((uint32_t *)(buffer.data() + curOffset)) =
htonl(blob->size());
curOffset += 4;
memcpy(buffer.data() + curOffset, blob->data(), blob->size());
curOffset = (curOffset + 3 + blob->size()) & ~0x3;
success = true;
}
void operator()(const OSCTrue &) { success = true; }
void operator()(const OSCFalse &) { success = true; }
void operator()(const OSCInfinity &) { success = true; }
void operator()(const OSCNull &) { success = true; }
};
OSCBlob::OSCBlob(const std::string &stringRepresentation)
: _stringRep(stringRepresentation)
{
}
void OSCBlob::SetStringRepresentation(const StringVariable &s)
{
_stringRep = s;
}
std::string OSCBlob::GetStringRepresentation() const
{
return _stringRep;
}
std::optional<std::vector<char>> OSCBlob::GetBinary() const
{
std::vector<char> bytes;
std::string hexString = _stringRep;
for (std::size_t i = 2; i < hexString.size(); i += 4) {
try {
auto byteString = hexString.substr(i, 2);
int value = std::stoi(byteString, nullptr, 16);
bytes.push_back(static_cast<char>(value));
} catch (const std::exception &e) {
blog(LOG_WARNING,
"failed to convert hex \"%s\" to binary: %s",
hexString.c_str(), e.what());
return {};
}
}
return bytes;
}
void OSCBlob::Save(obs_data_t *obj, const char *name) const
{
_stringRep.Save(obj, name);
}
void OSCBlob::Load(obs_data_t *obj, const char *name)
{
_stringRep.Load(obj, name);
}
void OSCTrue::Save(obs_data_t *obj, const char *name) const
{
obs_data_set_bool(obj, name, true);
}
void OSCFalse::Save(obs_data_t *obj, const char *name) const
{
obs_data_set_bool(obj, name, true);
}
void OSCInfinity::Save(obs_data_t *obj, const char *name) const
{
obs_data_set_bool(obj, name, true);
}
void OSCNull::Save(obs_data_t *obj, const char *name) const
{
obs_data_set_bool(obj, name, true);
}
std::optional<std::vector<char>> OSCMessage::GetBuffer() const
{
if (std::string(_address).empty()) {
return {};
}
std::vector<char> buffer(128, 0);
uint32_t currentOffset = (uint32_t)strlen(_address.c_str());
if (currentOffset > buffer.size()) {
buffer.resize(buffer.size() * 2);
}
strncpy(buffer.data(), _address.c_str(), buffer.size());
currentOffset = (currentOffset + 4) & ~0x3;
std::string typeTags;
for (const auto &e : _elements) {
typeTags += e.GetTypeTag();
}
buffer.at(currentOffset++) = ',';
int length = (int)strlen(typeTags.c_str());
if (currentOffset + length >= buffer.size()) {
buffer.resize(buffer.size() * 2);
}
strncpy(buffer.data() + currentOffset, typeTags.c_str(),
buffer.size() - currentOffset - length);
currentOffset = (currentOffset + 4 + length) & ~0x3;
for (const auto &e : _elements) {
const auto &value = e._value;
FillMessageElementBufferVisitor visitor{buffer, currentOffset};
std::visit(visitor, value);
if (!visitor.success) {
return {};
}
}
buffer.resize(currentOffset);
return buffer;
}
const char *OSCMessageElement::GetTypeTag() const
{
return GetTypeTag(*this);
}
const char *OSCMessageElement::GetTypeName() const
{
return GetTypeName(*this);
}
const char *OSCMessageElement::GetTypeTag(const OSCMessageElement &element)
{
return _typeNames.at(element._value.index()).tag;
}
const char *OSCMessageElement::GetTypeName(const OSCMessageElement &element)
{
return obs_module_text(
_typeNames.at(element._value.index()).localizedName);
}
void OSCMessageElement::Save(obs_data_t *obj) const
{
std::visit(
[obj](auto &&arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, StringVariable>) {
arg.Save(obj, "strValue");
} else if constexpr (std::is_same_v<T, IntVariable>) {
arg.Save(obj, "intValue");
} else if constexpr (std::is_same_v<T, DoubleVariable>) {
arg.Save(obj, "floatValue");
} else if constexpr (std::is_same_v<T, OSCBlob>) {
arg.Save(obj, "binaryValue");
} else if constexpr (std::is_same_v<T, OSCTrue>) {
arg.Save(obj, "trueValue");
} else if constexpr (std::is_same_v<T, OSCFalse>) {
arg.Save(obj, "falseValue");
} else if constexpr (std::is_same_v<T, OSCInfinity>) {
arg.Save(obj, "infiniteValue");
} else if constexpr (std::is_same_v<T, OSCNull>) {
arg.Save(obj, "nullValue");
} else {
blog(LOG_WARNING,
"cannot save unkown OSCMessageElement");
}
},
_value);
}
void OSCMessageElement::Load(obs_data_t *obj)
{
if (obs_data_has_user_value(obj, "strValue")) {
StringVariable string;
string.Load(obj, "strValue");
_value = string;
} else if (obs_data_has_user_value(obj, "intValue")) {
NumberVariable<int> intValue;
intValue.Load(obj, "intValue");
_value = intValue;
} else if (obs_data_has_user_value(obj, "floatValue")) {
NumberVariable<double> floatValue;
floatValue.Load(obj, "floatValue");
_value = floatValue;
} else if (obs_data_has_user_value(obj, "binaryValue")) {
OSCBlob binValue;
binValue.Load(obj, "binaryValue");
_value = binValue;
} else if (obs_data_has_user_value(obj, "trueValue")) {
_value = OSCTrue();
} else if (obs_data_has_user_value(obj, "falseValue")) {
_value = OSCFalse();
} else if (obs_data_has_user_value(obj, "OSCInfinite")) {
_value = OSCInfinity();
} else if (obs_data_has_user_value(obj, "nullValue")) {
_value = OSCNull();
} else {
blog(LOG_WARNING, "cannot load unkown OSCMessageElement");
}
}
std::string OSCMessageElement::ToString() const
{
return std::visit(
[](auto &&arg) -> std::string {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, StringVariable>) {
return arg;
} else if constexpr (std::is_same_v<T, OSCBlob> ||
std::is_same_v<T, OSCTrue> ||
std::is_same_v<T, OSCFalse> ||
std::is_same_v<T, OSCInfinity> ||
std::is_same_v<T, OSCNull>) {
return arg.GetStringRepresentation();
} else {
return std::to_string(arg.GetValue());
}
},
_value);
}
void OSCMessage::Save(obs_data_t *obj) const
{
auto data = obs_data_create();
_address.Save(data, "address");
auto elements = obs_data_array_create();
for (const auto &e : _elements) {
auto array_obj = obs_data_create();
e.Save(array_obj);
obs_data_array_push_back(elements, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(data, "elements", elements);
obs_data_set_obj(obj, "oscMessage", data);
obs_data_array_release(elements);
obs_data_release(data);
}
void OSCMessage::Load(obs_data_t *obj)
{
auto data = obs_data_get_obj(obj, "oscMessage");
_address.Load(data, "address");
_elements.clear();
auto elements = obs_data_get_array(data, "elements");
size_t count = obs_data_array_count(elements);
for (size_t i = 0; i < count; i++) {
auto array_obj = obs_data_array_item(elements, i);
OSCMessageElement e;
e.Load(array_obj);
_elements.push_back(e);
obs_data_release(array_obj);
}
obs_data_array_release(elements);
obs_data_release(data);
}
std::string OSCMessage::ToString() const
{
std::string res = "address: " + std::string(_address) + " message: ";
for (const auto &e : _elements) {
res += "[" + e.ToString() + "]";
}
return res;
}
OSCMessageElementEdit::OSCMessageElementEdit(QWidget *parent)
: QWidget(parent),
_type(new QComboBox(this)),
_intValue(new VariableSpinBox(this)),
_doubleValue(new VariableDoubleSpinBox(this)),
_text(new VariableLineEdit(this)),
_binaryText(new VariableLineEdit(this))
{
installEventFilter(this);
_intValue->setMinimum(INT_MIN);
_intValue->setMaximum(INT_MAX);
_doubleValue->setMinimum(-9999999999);
_doubleValue->setMaximum(9999999999);
_doubleValue->setDecimals(10);
_intValue->hide();
_doubleValue->hide();
_text->hide();
_binaryText->hide();
for (int i = 0; i < OSCMessageElement::_typeNames.size() - 1; i++) {
_type->addItem(obs_module_text(
OSCMessageElement::_typeNames.at(i).localizedName));
}
_type->setCurrentIndex(0);
QWidget::connect(_type, SIGNAL(currentIndexChanged(int)), this,
SLOT(TypeChanged(int)));
QWidget::connect(
_doubleValue,
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this, SLOT(DoubleChanged(const NumberVariable<double> &)));
QWidget::connect(
_intValue,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(IntChanged(const NumberVariable<int> &)));
QWidget::connect(_text, SIGNAL(editingFinished()), this,
SLOT(TextChanged()));
QWidget::connect(_binaryText, SIGNAL(editingFinished()), this,
SLOT(BinaryTextChanged()));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_type, 1);
layout->addWidget(_intValue, 4);
layout->addWidget(_doubleValue, 4);
layout->addWidget(_text, 4);
layout->addWidget(_binaryText, 4);
setLayout(layout);
}
void OSCMessageElementEdit::SetMessageElement(const OSCMessageElement &element)
{
const QSignalBlocker b(this);
_type->setCurrentText(element.GetTypeName());
SetVisibility(element);
if (std::holds_alternative<StringVariable>(element._value)) {
_text->setText(std::get<StringVariable>(element._value));
} else if (std::holds_alternative<IntVariable>(element._value)) {
_intValue->SetValue(std::get<IntVariable>(element._value));
} else if (std::holds_alternative<DoubleVariable>(element._value)) {
_doubleValue->SetValue(
std::get<DoubleVariable>(element._value));
} else if (std::holds_alternative<OSCBlob>(element._value)) {
_binaryText->setText(std::get<OSCBlob>(element._value)
.GetStringRepresentation());
}
}
bool OSCMessageElementEdit::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonDblClick) {
emit Focussed();
}
return QWidget::eventFilter(obj, event);
}
void OSCMessageElementEdit::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
QWidgetList childWidgets = findChildren<QWidget *>();
for (QWidget *childWidget : childWidgets) {
childWidget->installEventFilter(this);
}
}
void OSCMessageElementEdit::DoubleChanged(const DoubleVariable &value)
{
emit ElementValueChanged(OSCMessageElement(value));
}
void OSCMessageElementEdit::IntChanged(const IntVariable &value)
{
emit ElementValueChanged(OSCMessageElement(value));
}
void OSCMessageElementEdit::TextChanged()
{
emit ElementValueChanged(StringVariable(_text->text().toStdString()));
}
void OSCMessageElementEdit::BinaryTextChanged()
{
emit ElementValueChanged(OSCBlob(_binaryText->text().toStdString()));
}
void OSCMessageElementEdit::SetVisibility(const OSCMessageElement &element)
{
_intValue->hide();
_doubleValue->hide();
_text->hide();
_binaryText->hide();
if (std::holds_alternative<StringVariable>(element._value)) {
_text->show();
} else if (std::holds_alternative<IntVariable>(element._value)) {
_intValue->show();
} else if (std::holds_alternative<DoubleVariable>(element._value)) {
_doubleValue->show();
} else if (std::holds_alternative<OSCBlob>(element._value)) {
_binaryText->show();
}
}
void OSCMessageElementEdit::TypeChanged(int idx)
{
OSCMessageElement element;
if (idx == 0) {
element = OSCMessageElement(IntVariable(0));
} else if (idx == 1) {
element = OSCMessageElement(DoubleVariable(0.0));
} else if (idx == 2) {
element = OSCMessageElement("value");
} else if (idx == 3) {
element = OSCMessageElement(OSCBlob("\\x00\\x01\\x02\\x03"));
} else if (idx == 4) {
element = OSCMessageElement(OSCTrue());
} else if (idx == 5) {
element = OSCMessageElement(OSCFalse());
} else if (idx == 6) {
element = OSCMessageElement(OSCInfinity());
} else if (idx == 7) {
element = OSCMessageElement(OSCNull());
}
SetVisibility(element);
SetMessageElement(element);
emit ElementValueChanged(element);
}
OSCMessageEdit::OSCMessageEdit(QWidget *parent)
: QWidget(parent),
_address(new VariableLineEdit(this)),
_elements(new QListWidget()),
_add(new QPushButton()),
_remove(new QPushButton()),
_up(new QPushButton()),
_down(new QPushButton())
{
_elements->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_elements->setAutoScroll(false);
_add->setMaximumWidth(22);
_add->setProperty("themeID",
QVariant(QString::fromUtf8("addIconSmall")));
_add->setFlat(true);
_remove->setMaximumWidth(22);
_remove->setProperty("themeID",
QVariant(QString::fromUtf8("removeIconSmall")));
_remove->setFlat(true);
_up->setMaximumWidth(22);
_up->setProperty("themeID",
QVariant(QString::fromUtf8("upArrowIconSmall")));
_up->setFlat(true);
_down->setMaximumWidth(22);
_down->setProperty("themeID",
QVariant(QString::fromUtf8("downArrowIconSmall")));
_down->setFlat(true);
QWidget::connect(_address, SIGNAL(editingFinished()), this,
SLOT(AddressChanged()));
QWidget::connect(_add, SIGNAL(clicked()), this, SLOT(Add()));
QWidget::connect(_remove, SIGNAL(clicked()), this, SLOT(Remove()));
QWidget::connect(_up, SIGNAL(clicked()), this, SLOT(Up()));
QWidget::connect(_down, SIGNAL(clicked()), this, SLOT(Down()));
auto controlsLayout = new QHBoxLayout();
controlsLayout->addWidget(_add);
controlsLayout->addWidget(_remove);
QFrame *line = new QFrame();
line->setFrameShape(QFrame::VLine);
line->setFrameShadow(QFrame::Sunken);
controlsLayout->addWidget(line);
controlsLayout->addWidget(_up);
controlsLayout->addWidget(_down);
controlsLayout->addStretch();
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_address);
layout->addWidget(_elements);
layout->addLayout(controlsLayout);
setLayout(layout);
}
void OSCMessageEdit::InsertElement(const OSCMessageElement &element)
{
auto item = new QListWidgetItem(_elements);
_elements->addItem(item);
auto elementEdit = new OSCMessageElementEdit(this);
elementEdit->SetMessageElement(element);
item->setSizeHint(elementEdit->minimumSizeHint());
_elements->setItemWidget(item, elementEdit);
QWidget::connect(elementEdit,
SIGNAL(ElementValueChanged(const OSCMessageElement &)),
this,
SLOT(ElementValueChanged(const OSCMessageElement &)));
QWidget::connect(elementEdit, SIGNAL(Focussed()), this,
SLOT(ElementFocussed()));
_currentSelection._elements.push_back(element);
}
void OSCMessageEdit::SetMessage(const OSCMessage &message)
{
_address->setText(message._address);
for (const auto &element : message._elements) {
InsertElement(element);
}
_currentSelection = message;
SetWidgetSize();
}
void OSCMessageEdit::AddressChanged()
{
_currentSelection._address = _address->text().toStdString();
emit MessageChanged(_currentSelection);
}
void OSCMessageEdit::Add()
{
OSCMessageElement element;
InsertElement(element);
emit MessageChanged(_currentSelection);
SetWidgetSize();
}
void OSCMessageEdit::Remove()
{
auto item = _elements->currentItem();
int idx = _elements->currentRow();
if (!item || idx == -1) {
return;
}
delete item;
_currentSelection._elements.erase(_currentSelection._elements.begin() +
idx);
emit MessageChanged(_currentSelection);
SetWidgetSize();
}
void OSCMessageEdit::Up()
{
int idx = _elements->currentRow();
if (!listMoveUp(_elements)) {
return;
}
iter_swap(_currentSelection._elements.begin() + idx,
_currentSelection._elements.begin() + idx - 1);
emit MessageChanged(_currentSelection);
SetWidgetSize();
}
void OSCMessageEdit::Down()
{
int idx = _elements->currentRow();
if (!listMoveDown(_elements)) {
return;
}
iter_swap(_currentSelection._elements.begin() + idx,
_currentSelection._elements.begin() + idx + 1);
emit MessageChanged(_currentSelection);
SetWidgetSize();
}
static QListWidgetItem *getItemFromWidget(QListWidget *list, QWidget *widget)
{
for (int i = 0; i < list->count(); i++) {
auto item = list->item(i);
if (!item) {
continue;
}
auto itemWidget = list->itemWidget(item);
if (itemWidget == widget) {
return item;
}
}
return nullptr;
}
int OSCMessageEdit::GetIndexOfSignal()
{
auto sender = this->sender();
if (!sender) {
return -1;
}
auto widget = qobject_cast<QWidget *>(sender);
if (!widget) {
return -1;
}
return _elements->row(getItemFromWidget(_elements, widget));
}
void OSCMessageEdit::ElementFocussed()
{
int idx = GetIndexOfSignal();
if (idx == -1) {
return;
}
_elements->setCurrentRow(idx);
}
void OSCMessageEdit::ElementValueChanged(const OSCMessageElement &element)
{
int idx = GetIndexOfSignal();
if (idx == -1) {
return;
}
_currentSelection._elements.at(idx) = element;
_elements->setCurrentRow(idx);
emit MessageChanged(_currentSelection);
}
void OSCMessageEdit::SetWidgetSize()
{
SetHeightToContentHeight(_elements);
adjustSize();
updateGeometry();
}
} // namespace advss

168
src/utils/osc-helpers.hpp Normal file
View File

@ -0,0 +1,168 @@
#pragma once
#include "variable-string.hpp"
#include "variable-number.hpp"
#include "variable-line-edit.hpp"
#include "variable-spinbox.hpp"
#include <variant>
#include <unordered_map>
#include <QListWidget>
namespace advss {
class OSCBlob {
public:
OSCBlob() = default;
OSCBlob(const std::string &stringRepresentation);
void SetStringRepresentation(const StringVariable &);
std::string GetStringRepresentation() const;
std::optional<std::vector<char>> GetBinary() const;
void Save(obs_data_t *obj, const char *name) const;
void Load(obs_data_t *obj, const char *name);
private:
StringVariable _stringRep;
};
class OSCTrue {
public:
void Save(obs_data_t *obj, const char *name) const;
std::string GetStringRepresentation() const { return "true"; }
};
class OSCFalse {
public:
void Save(obs_data_t *obj, const char *name) const;
std::string GetStringRepresentation() const { return "false"; }
};
class OSCInfinity {
public:
void Save(obs_data_t *obj, const char *name) const;
std::string GetStringRepresentation() const { return "infinity"; }
};
class OSCNull {
public:
void Save(obs_data_t *obj, const char *name) const;
std::string GetStringRepresentation() const { return "null"; }
};
class OSCMessageElement {
public:
OSCMessageElement() = default;
OSCMessageElement(const StringVariable &v) : _value(v) {}
OSCMessageElement(const IntVariable &v) : _value(v) {}
OSCMessageElement(const DoubleVariable &v) : _value(v) {}
OSCMessageElement(const OSCBlob &v) : _value(v) {}
OSCMessageElement(const OSCTrue &v) : _value(v) {}
OSCMessageElement(const OSCFalse &v) : _value(v) {}
OSCMessageElement(const OSCInfinity &v) : _value(v) {}
OSCMessageElement(const OSCNull &v) : _value(v) {}
void Save(obs_data_t *obj) const;
void Load(obs_data_t *obj);
std::string ToString() const;
const char *GetTypeName() const;
const char *GetTypeTag() const;
static const char *GetTypeName(const OSCMessageElement &);
static const char *GetTypeTag(const OSCMessageElement &);
private:
struct TypeInfo {
const char *localizedName, *tag;
};
static std::unordered_map<size_t, TypeInfo> _typeNames;
std::variant<IntVariable, DoubleVariable, StringVariable, OSCBlob,
OSCTrue, OSCFalse, OSCInfinity, OSCNull>
_value;
friend class OSCMessage;
friend class OSCMessageElementEdit;
};
class OSCMessage {
public:
void Save(obs_data_t *obj) const;
void Load(obs_data_t *obj);
std::string ToString() const;
std::optional<std::vector<char>> GetBuffer() const;
private:
StringVariable _address = "/address";
std::vector<OSCMessageElement> _elements = {
OSCMessageElement("example"),
OSCMessageElement(IntVariable(3))};
friend class OSCMessageEdit;
};
class OSCMessageElementEdit : public QWidget {
Q_OBJECT
public:
OSCMessageElementEdit(QWidget *);
void SetMessageElement(const OSCMessageElement &);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
void showEvent(QShowEvent *event) override;
private slots:
void TypeChanged(int);
void DoubleChanged(const NumberVariable<double> &value);
void IntChanged(const NumberVariable<int> &value);
void TextChanged();
void BinaryTextChanged();
signals:
void ElementValueChanged(const OSCMessageElement &);
void Focussed();
private:
void SetVisibility(const OSCMessageElement &);
QComboBox *_type;
VariableSpinBox *_intValue;
VariableDoubleSpinBox *_doubleValue;
VariableLineEdit *_text;
VariableLineEdit *_binaryText;
};
class OSCMessageEdit : public QWidget {
Q_OBJECT
public:
OSCMessageEdit(QWidget *);
void SetMessage(const OSCMessage &);
private slots:
void ElementValueChanged(const OSCMessageElement &);
void ElementFocussed();
void AddressChanged();
void Add();
void Remove();
void Up();
void Down();
signals:
void MessageChanged(const OSCMessage &);
private:
void InsertElement(const OSCMessageElement &);
void SetWidgetSize();
int GetIndexOfSignal();
VariableLineEdit *_address;
QListWidget *_elements;
QPushButton *_add;
QPushButton *_remove;
QPushButton *_up;
QPushButton *_down;
OSCMessage _currentSelection;
};
} // namespace advss

View File

@ -1138,9 +1138,13 @@ void SetHeightToContentHeight(QListWidget *list)
auto nrItems = list->count();
if (nrItems == 0) {
list->setMaximumHeight(0);
list->setMinimumHeight(0);
} else {
list->setMaximumHeight(list->sizeHintForRow(0) * nrItems +
2 * list->frameWidth());
int height =
(list->sizeHintForRow(0) + list->spacing()) * nrItems +
2 * list->frameWidth();
list->setMinimumHeight(height);
list->setMaximumHeight(height);
}
}